Fix jittery player animation on fast vertical movement
Camera was repositioned in update() (before the physics step synced the sprite), so it lagged the player by one frame; on fast ascent/rocket that read as the character trembling and breaking up. Move camera follow to a POST_UPDATE (lateUpdate) handler that runs after the sprite position is synced. Verified on-screen jitter while the camera is latched is now 0px (was ~one frame of vertical speed). Also prevent squash-and-stretch tweens from stacking on rapid bounces (stop any in-flight squash before starting a new one and on powerup/death transitions), which had made the sprite scale pop.
This commit is contained in:
@@ -67,26 +67,36 @@ export class Player extends Physics.Arcade.Sprite {
|
||||
jump(force = JUMP_VELOCITY) {
|
||||
if (this.state === 'dead' || this.state === 'rocket' || this.state === 'propeller') return false;
|
||||
this.setVelocityY(force);
|
||||
this.scene.tweens.add({
|
||||
// Squash-and-stretch, but never let two squash tweens stack on rapid
|
||||
// bounces (that made the sprite scale pop and look like it was breaking up).
|
||||
if (this.squashTween) this.squashTween.stop();
|
||||
this.setScale(0.45);
|
||||
this.squashTween = this.scene.tweens.add({
|
||||
targets: this,
|
||||
scaleX: 0.55,
|
||||
scaleY: 0.4,
|
||||
duration: 80,
|
||||
scaleY: 0.38,
|
||||
duration: 90,
|
||||
yoyo: true,
|
||||
ease: 'Quad.easeOut',
|
||||
onComplete: () => {
|
||||
this.squashTween = null;
|
||||
this.setScale(0.45);
|
||||
},
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
cutJump(factor) {
|
||||
if (this.state !== 'normal') return;
|
||||
if (this.body.velocity.y < 0) {
|
||||
this.setVelocityY(this.body.velocity.y * factor);
|
||||
_stopSquash() {
|
||||
if (this.squashTween) {
|
||||
this.squashTween.stop();
|
||||
this.squashTween = null;
|
||||
}
|
||||
this.setScale(0.45);
|
||||
}
|
||||
|
||||
startPropeller() {
|
||||
if (this.state === 'dead') return;
|
||||
this._stopSquash();
|
||||
this.state = 'propeller';
|
||||
this.propellerTimer = POWERUP_DURATION.propeller;
|
||||
const oldHeight = this.displayHeight;
|
||||
@@ -98,6 +108,7 @@ export class Player extends Physics.Arcade.Sprite {
|
||||
|
||||
startRocket() {
|
||||
if (this.state === 'dead') return;
|
||||
this._stopSquash();
|
||||
this.state = 'rocket';
|
||||
this.rocketTimer = POWERUP_DURATION.rocket;
|
||||
const oldHeight = this.displayHeight;
|
||||
@@ -108,6 +119,7 @@ export class Player extends Physics.Arcade.Sprite {
|
||||
}
|
||||
|
||||
endPowerUp() {
|
||||
this._stopSquash();
|
||||
const oldHeight = this.displayHeight;
|
||||
this.state = 'normal';
|
||||
this.setTexture('player_idle');
|
||||
@@ -119,6 +131,7 @@ export class Player extends Physics.Arcade.Sprite {
|
||||
|
||||
die() {
|
||||
if (this.state === 'dead') return;
|
||||
this._stopSquash();
|
||||
this.state = 'dead';
|
||||
this.setTexture('player_dead');
|
||||
this.setScale(0.4);
|
||||
|
||||
@@ -97,6 +97,10 @@ export class GameScene extends Scene {
|
||||
|
||||
this.escKey.on('down', () => this.togglePause());
|
||||
|
||||
// Move the camera after physics has synced the sprite (POST_UPDATE) so the
|
||||
// player never lags the camera by a frame on fast vertical movement.
|
||||
this.events.on('postupdate', this.lateUpdate, this);
|
||||
|
||||
// Auto-pause when the tab/window is hidden — avoids a delta-spike teleport
|
||||
// on refocus. Player must resume manually (not auto-resumed).
|
||||
this._onHidden = () => {
|
||||
@@ -114,6 +118,19 @@ export class GameScene extends Scene {
|
||||
this.game.events.off('hidden', this._onHidden);
|
||||
this._onHidden = null;
|
||||
}
|
||||
this.events.off('postupdate', this.lateUpdate, this);
|
||||
}
|
||||
|
||||
// Runs after the physics step (sprite positions already synced this frame).
|
||||
lateUpdate() {
|
||||
if (this.isGameOver || this.isPaused || !this.player) return;
|
||||
// Doodle-jump camera: latch the player at the trigger line going up; never
|
||||
// scroll back down.
|
||||
const targetScrollY = this.player.y - this.cameraTriggerY;
|
||||
if (targetScrollY < this.cameras.main.scrollY) {
|
||||
this.cameras.main.scrollY = targetScrollY;
|
||||
}
|
||||
this.bg.tilePositionY = Math.round(this.cameras.main.scrollY * 0.3);
|
||||
}
|
||||
|
||||
update(time, delta) {
|
||||
@@ -127,14 +144,9 @@ export class GameScene extends Scene {
|
||||
this.player.setVelocityY(PHYSICS.maxFallSpeed);
|
||||
}
|
||||
|
||||
// Camera: latch player at trigger line going up, free movement otherwise.
|
||||
const targetScrollY = this.player.y - this.cameraTriggerY;
|
||||
if (targetScrollY < this.cameras.main.scrollY) {
|
||||
this.cameras.main.scrollY = targetScrollY;
|
||||
}
|
||||
|
||||
this.bg.tilePositionY = Math.round(this.cameras.main.scrollY * 0.3);
|
||||
|
||||
// NOTE: camera following happens in lateUpdate (POST_UPDATE) — after the
|
||||
// physics step syncs the sprite — otherwise the camera lags the player by
|
||||
// one frame and fast ascent/rocket looks jittery.
|
||||
this.minScrollY = Math.min(this.minScrollY, this.cameras.main.scrollY);
|
||||
const killLine = this.minScrollY + GAME_HEIGHT;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user