Phaser 动画系统 #

动画概述 #

Phaser 提供了两种主要的动画系统:帧动画(Sprite Animation)和补间动画(Tween)。

text
┌─────────────────────────────────────────────────────────────┐
│                    动画系统架构                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  帧动画 (Frame Animation)                                  │
│  ├── 精灵表动画                                            │
│  ├── 按帧播放                                              │
│  ├── 适合角色动作                                          │
│  └── 由 Animation Manager 管理                             │
│                                                             │
│  补间动画 (Tween)                                          │
│  ├── 属性插值                                              │
│  ├── 平滑过渡                                              │
│  ├── 适合移动、缩放、透明度                                │
│  └── 由 Tween Manager 管理                                 │
│                                                             │
└─────────────────────────────────────────────────────────────┘

帧动画 #

加载精灵表 #

javascript
preload() {
  this.load.spritesheet('dude', 'assets/dude.png', {
    frameWidth: 32,
    frameHeight: 48
  });
  
  this.load.spritesheet('explosion', 'assets/explosion.png', {
    frameWidth: 64,
    frameHeight: 64,
    endFrame: 16
  });
  
  this.load.atlas('game', 'assets/game.png', 'assets/game.json');
}

创建动画 #

javascript
create() {
  this.anims.create({
    key: 'left',
    frames: this.anims.generateFrameNumbers('dude', { start: 0, end: 3 }),
    frameRate: 10,
    repeat: -1
  });
  
  this.anims.create({
    key: 'turn',
    frames: [ { key: 'dude', frame: 4 } ],
    frameRate: 20
  });
  
  this.anims.create({
    key: 'right',
    frames: this.anims.generateFrameNumbers('dude', { start: 5, end: 8 }),
    frameRate: 10,
    repeat: -1
  });
}

动画配置 #

javascript
this.anims.create({
  key: 'walk',
  frames: frames,
  frameRate: 12,
  duration: null,
  repeat: -1,
  repeatDelay: 0,
  yoyo: false,
  delay: 0,
  showOnStart: false,
  hideOnComplete: false,
  skipMissedFrames: true
});

生成帧 #

javascript
const frames = this.anims.generateFrameNumbers('dude', {
  start: 0,
  end: 7
});

const frames = this.anims.generateFrameNumbers('dude', {
  start: 0,
  end: 7,
  first: 4
});

const frames = this.anims.generateFrameNumbers('dude', {
  frames: [0, 1, 2, 3, 4, 3, 2, 1, 0]
});

const frames = this.anims.generateFrameNames('atlas', {
  prefix: 'walk_',
  start: 0,
  end: 7,
  suffix: '.png'
});

const frames = this.anims.generateFrameNames('atlas', {
  prefix: 'player_',
  suffix: '.png',
  zeroPad: 2,
  start: 1,
  end: 10
});

const frames = this.anims.generateFrameNames('atlas', {
  frames: ['walk_01', 'walk_02', 'walk_03']
});

播放动画 #

javascript
const sprite = this.add.sprite(100, 100, 'dude');

sprite.anims.play('left');

sprite.anims.play('left', true);

sprite.anims.play('left', false, 0);

sprite.anims.playAfterDelay('right', 2000);

sprite.anims.playAfterRepeat('turn', 3);

sprite.anims.chain(['walk', 'run', 'jump']);

sprite.anims.stop();

sprite.anims.stopOnFrame(sprite.anims.currentAnim.frames[3]);

sprite.anims.pause();
sprite.anims.resume();

sprite.anims.restart();

动画事件 #

javascript
const sprite = this.add.sprite(100, 100, 'dude');

sprite.on('animationstart', (animation, frame) => {
  console.log('Animation started:', animation.key);
});

sprite.on('animationcomplete', (animation, frame) => {
  console.log('Animation completed:', animation.key);
});

sprite.on('animationcomplete-walk', (animation, frame) => {
  console.log('Walk animation completed');
});

sprite.on('animationrepeat', (animation, frame) => {
  console.log('Animation repeated:', animation.key);
});

sprite.on('animationupdate', (animation, frame, gameObject) => {
  console.log('Frame updated:', frame.textureFrame);
});

sprite.on('animationstop', (animation, frame) => {
  console.log('Animation stopped:', animation.key);
});

动画管理 #

javascript
const anim = this.anims.get('walk');

const exists = this.anims.exists('walk');

this.anims.remove('walk');

const allAnims = this.anims.getAll();

this.anims.pauseAll();
this.anims.resumeAll();

this.anims.getGlobalTimeScale();
this.anims.setGlobalTimeScale(0.5);

动画实例属性 #

javascript
const sprite = this.add.sprite(100, 100, 'dude');
sprite.anims.play('walk');

const currentAnim = sprite.anims.currentAnim;
const currentFrame = sprite.anims.currentFrame;
const frameRate = sprite.anims.frameRate;
const duration = sprite.anims.duration;
const msPerFrame = sprite.anims.msPerFrame;
const progress = sprite.anims.progress;
const totalFrames = sprite.anims.totalFrames;

sprite.anims.setTimeScale(2);
sprite.anims.setDelay(1000);
sprite.anims.setRepeat(5);
sprite.anims.setRepeatDelay(500);

补间动画(Tween) #

基本补间 #

javascript
this.tweens.add({
  targets: this.player,
  x: 400,
  duration: 2000,
  ease: 'Power2'
});

this.tweens.add({
  targets: this.player,
  x: 400,
  y: 300,
  duration: 2000
});

补间配置 #

javascript
this.tweens.add({
  targets: this.player,
  x: 400,
  y: 300,
  duration: 2000,
  ease: 'Power2',
  delay: 0,
  repeat: 0,
  repeatDelay: 0,
  yoyo: false,
  hold: 0,
  paused: false,
  onStart: function(tween, targets) {
    console.log('Tween started');
  },
  onUpdate: function(tween, targets) {
    console.log('Tween updating');
  },
  onComplete: function(tween, targets) {
    console.log('Tween completed');
  },
  onRepeat: function(tween, targets) {
    console.log('Tween repeated');
  },
  onYoyo: function(tween, targets) {
    console.log('Tween yoyo');
  },
  callbackScope: this
});

缓动函数 #

javascript
this.tweens.add({
  targets: this.player,
  x: 400,
  ease: 'Linear',
  duration: 2000
});

this.tweens.add({
  targets: this.player,
  x: 400,
  ease: 'Power0',
  duration: 2000
});

this.tweens.add({
  targets: this.player,
  x: 400,
  ease: 'Power1',
  duration: 2000
});

this.tweens.add({
  targets: this.player,
  x: 400,
  ease: 'Power2',
  duration: 2000
});

this.tweens.add({
  targets: this.player,
  x: 400,
  ease: 'Power3',
  duration: 2000
});

this.tweens.add({
  targets: this.player,
  x: 400,
  ease: 'Power4',
  duration: 2000
});

this.tweens.add({
  targets: this.player,
  x: 400,
  ease: 'Quad.easeOut',
  duration: 2000
});

this.tweens.add({
  targets: this.player,
  x: 400,
  ease: 'Cubic.easeInOut',
  duration: 2000
});

this.tweens.add({
  targets: this.player,
  x: 400,
  ease: 'Bounce.easeOut',
  duration: 2000
});

this.tweens.add({
  targets: this.player,
  x: 400,
  ease: 'Elastic.easeOut',
  duration: 2000
});

this.tweens.add({
  targets: this.player,
  x: 400,
  ease: 'Back.easeOut',
  duration: 2000
});

this.tweens.add({
  targets: this.player,
  x: 400,
  ease: 'Sine.easeInOut',
  duration: 2000
});

this.tweens.add({
  targets: this.player,
  x: 400,
  ease: 'Expo.easeOut',
  duration: 2000
});

this.tweens.add({
  targets: this.player,
  x: 400,
  ease: 'Circular.easeInOut',
  duration: 2000
});

补间属性 #

javascript
this.tweens.add({
  targets: this.player,
  x: 400,
  y: 300,
  duration: 2000
});

this.tweens.add({
  targets: this.player,
  alpha: 0,
  duration: 1000
});

this.tweens.add({
  targets: this.player,
  scaleX: 2,
  scaleY: 2,
  duration: 1000
});

this.tweens.add({
  targets: this.player,
  angle: 360,
  duration: 2000
});

this.tweens.add({
  targets: this.player,
  rotation: Math.PI * 2,
  duration: 2000
});

this.tweens.add({
  targets: this.player,
  tint: 0xff0000,
  duration: 1000
});

相对值 #

javascript
this.tweens.add({
  targets: this.player,
  x: '+=100',
  duration: 1000
});

this.tweens.add({
  targets: this.player,
  x: '-=50',
  y: '+=100',
  duration: 1000
});

多目标补间 #

javascript
this.tweens.add({
  targets: [this.player, this.enemy, this.npc],
  x: 400,
  y: 300,
  duration: 2000
});

this.tweens.add({
  targets: this.enemies.getChildren(),
  alpha: 0,
  duration: 1000
});

补间事件 #

javascript
this.tweens.add({
  targets: this.player,
  x: 400,
  duration: 2000,
  onStart: (tween, targets) => {
    console.log('Started');
  },
  onUpdate: (tween, targets) => {
    console.log('Progress:', tween.progress);
  },
  onComplete: (tween, targets) => {
    console.log('Completed');
  },
  onRepeat: (tween, targets) => {
    console.log('Repeated');
  },
  onYoyo: (tween, targets) => {
    console.log('Yoyo');
  },
  onPause: (tween, targets) => {
    console.log('Paused');
  },
  onResume: (tween, targets) => {
    console.log('Resumed');
  },
  onLoop: (tween, targets) => {
    console.log('Looped');
  }
});

补间控制 #

javascript
const tween = this.tweens.add({
  targets: this.player,
  x: 400,
  duration: 2000
});

tween.pause();

tween.resume();

tween.stop();

tween.restart();

tween.complete();

tween.seek(0.5);

const progress = tween.progress;
tween.setProgress(0.5);

const elapsed = tween.elapsed;
tween.setElapsed(1000);

const timeScale = tween.timeScale;
tween.setTimeScale(2);

循环与 Yoyo #

javascript
this.tweens.add({
  targets: this.player,
  x: 400,
  duration: 1000,
  repeat: -1
});

this.tweens.add({
  targets: this.player,
  x: 400,
  duration: 1000,
  repeat: 3
});

this.tweens.add({
  targets: this.player,
  x: 400,
  duration: 1000,
  yoyo: true
});

this.tweens.add({
  targets: this.player,
  x: 400,
  duration: 1000,
  repeat: -1,
  yoyo: true
});

this.tweens.add({
  targets: this.player,
  x: 400,
  duration: 1000,
  repeat: 3,
  repeatDelay: 500,
  yoyo: true,
  hold: 200
});

延迟执行 #

javascript
this.tweens.add({
  targets: this.player,
  x: 400,
  duration: 1000,
  delay: 500
});

this.tweens.add({
  targets: [this.player, this.enemy, this.npc],
  x: 400,
  duration: 1000,
  delay: (target, key, value, index) => index * 200
});

时间线(Timeline) #

创建时间线 #

javascript
const timeline = this.tweens.createTimeline();

timeline.add({
  targets: this.player,
  x: 400,
  duration: 1000
});

timeline.add({
  targets: this.player,
  y: 300,
  duration: 1000
});

timeline.play();

时间线配置 #

javascript
const timeline = this.tweens.timeline({
  targets: this.player,
  tweens: [
    {
      x: 400,
      duration: 1000
    },
    {
      y: 300,
      duration: 1000
    },
    {
      alpha: 0,
      duration: 500
    }
  ]
});

timeline.play();

时间线事件 #

javascript
const timeline = this.tweens.timeline({
  targets: this.player,
  tweens: [
    { x: 400, duration: 1000 },
    { y: 300, duration: 1000 }
  ],
  onComplete: () => {
    console.log('Timeline complete');
  }
});

补间管理器 #

获取补间 #

javascript
const tweens = this.tweens.getAllTweens();

const tween = this.tweens.getTweensOf(this.player);

const exists = this.tweens.isTweening(this.player);

全局控制 #

javascript
this.tweens.pauseAll();

this.tweens.resumeAll();

this.tweens.stopAll();

this.tweens.killTweensOf(this.player);

this.tweens.killAll();

const timeScale = this.tweens.timeScale;
this.tweens.setTimeScale(0.5);

实用动画效果 #

淡入淡出 #

javascript
fadeIn(sprite, duration = 1000) {
  sprite.setAlpha(0);
  this.tweens.add({
    targets: sprite,
    alpha: 1,
    duration: duration,
    ease: 'Linear'
  });
}

fadeOut(sprite, duration = 1000) {
  this.tweens.add({
    targets: sprite,
    alpha: 0,
    duration: duration,
    ease: 'Linear'
  });
}

fadeOutAndDestroy(sprite, duration = 1000) {
  this.tweens.add({
    targets: sprite,
    alpha: 0,
    duration: duration,
    ease: 'Linear',
    onComplete: () => sprite.destroy()
  });
}

弹跳效果 #

javascript
bounce(sprite) {
  this.tweens.add({
    targets: sprite,
    scaleX: 1.2,
    scaleY: 0.8,
    duration: 100,
    yoyo: true,
    repeat: 2,
    ease: 'Power2'
  });
}

bounceIn(sprite) {
  sprite.setScale(0);
  this.tweens.add({
    targets: sprite,
    scaleX: 1,
    scaleY: 1,
    duration: 500,
    ease: 'Bounce.easeOut'
  });
}

抖动效果 #

javascript
shake(sprite, intensity = 10, duration = 100) {
  const originalX = sprite.x;
  const originalY = sprite.y;
  
  this.tweens.add({
    targets: sprite,
    x: originalX + intensity,
    y: originalY + intensity,
    duration: duration,
    yoyo: true,
    repeat: 5,
    ease: 'Linear',
    onComplete: () => {
      sprite.setPosition(originalX, originalY);
    }
  });
}

闪烁效果 #

javascript
flash(sprite, duration = 100, times = 3) {
  this.tweens.add({
    targets: sprite,
    alpha: 0,
    duration: duration,
    yoyo: true,
    repeat: times * 2 - 1,
    ease: 'Linear'
  });
}

flashTint(sprite, color = 0xffffff, duration = 100, times = 3) {
  let count = 0;
  const maxCount = times * 2;
  
  const timer = this.time.addEvent({
    delay: duration,
    callback: () => {
      if (count % 2 === 0) {
        sprite.setTint(color);
      } else {
        sprite.clearTint();
      }
      count++;
      if (count >= maxCount) {
        timer.remove();
        sprite.clearTint();
      }
    },
    loop: true
  });
}

脉冲效果 #

javascript
pulse(sprite, minScale = 0.9, maxScale = 1.1, duration = 500) {
  this.tweens.add({
    targets: sprite,
    scaleX: maxScale,
    scaleY: maxScale,
    duration: duration,
    yoyo: true,
    repeat: -1,
    ease: 'Sine.easeInOut'
  });
}

浮动效果 #

javascript
float(sprite, distance = 10, duration = 1000) {
  this.tweens.add({
    targets: sprite,
    y: sprite.y - distance,
    duration: duration,
    yoyo: true,
    repeat: -1,
    ease: 'Sine.easeInOut'
  });
}

旋转效果 #

javascript
rotate(sprite, duration = 2000) {
  this.tweens.add({
    targets: sprite,
    angle: 360,
    duration: duration,
    repeat: -1,
    ease: 'Linear'
  });
}

rotateBackAndForth(sprite, duration = 1000) {
  this.tweens.add({
    targets: sprite,
    angle: 15,
    duration: duration,
    yoyo: true,
    repeat: -1,
    ease: 'Sine.easeInOut'
  });
}

完整示例 #

UI 动画 #

javascript
class MenuScene extends Phaser.Scene {
  create() {
    this.createTitle();
    this.createButtons();
  }
  
  createTitle() {
    const title = this.add.text(400, 100, 'My Game', {
      fontSize: '64px',
      fill: '#fff'
    }).setOrigin(0.5).setAlpha(0);
    
    this.tweens.add({
      targets: title,
      alpha: 1,
      y: 150,
      duration: 1000,
      ease: 'Power2'
    });
    
    this.tweens.add({
      targets: title,
      y: title.y + 10,
      duration: 2000,
      yoyo: true,
      repeat: -1,
      ease: 'Sine.easeInOut'
    });
  }
  
  createButtons() {
    const buttons = [
      { text: 'Start Game', y: 300 },
      { text: 'Settings', y: 380 },
      { text: 'Credits', y: 460 }
    ];
    
    buttons.forEach((btn, index) => {
      const button = this.add.text(400, btn.y + 50, btn.text, {
        fontSize: '32px',
        fill: '#fff',
        backgroundColor: '#333',
        padding: { x: 20, y: 10 }
      }).setOrigin(0.5).setAlpha(0).setInteractive();
      
      this.tweens.add({
        targets: button,
        alpha: 1,
        y: btn.y,
        duration: 500,
        delay: 500 + index * 200,
        ease: 'Power2'
      });
      
      button.on('pointerover', () => {
        this.tweens.add({
          targets: button,
          scaleX: 1.1,
          scaleY: 1.1,
          duration: 100
        });
      });
      
      button.on('pointerout', () => {
        this.tweens.add({
          targets: button,
          scaleX: 1,
          scaleY: 1,
          duration: 100
        });
      });
    });
  }
}

下一步 #

现在你已经掌握了动画系统,接下来学习 瓦片地图,了解如何创建大型游戏世界!

最后更新:2026-03-29