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:
2026-06-01 01:53:33 +07:00
parent 9c07c52d34
commit 7c6a792212
2 changed files with 40 additions and 15 deletions

View File

@@ -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);

View File

@@ -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;