关于 phaser-animation
面向 Phaser 4 游戏开发者的动画技能,涵盖精灵帧动画、状态转换、补间和粒子效果。重点说明状态机的可靠处理,并附有详细参考指南。
主要功能
- 01精灵表与图集动画设置
- 02动画事件与串联播放
- 03角色状态机模式
- 04补间效果与时间轴
- 05粒子爆发示例
适用场景
- 01制作角色移动和攻击动画
- 02避免强制动画过早回退
- 03为界面元素和过场动画制作动效
- 04制作冲击和拾取爆发效果
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ yakoub-ai/ phaser4-gamedev --skill phaser-animation安装后在 Claude Code 中输入/phaser-animation确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/animation-api.mdeasing-reference.mdstate-machine-patterns.md详细介绍
Phaser 4 动画与补间
Phaser 4 有两种不同的动画系统:基于帧的精灵动画(在纹理图集或精灵表中循环播放各帧)和补间(随时间插值数值属性)。两者搭配使用,可让游戏体验更加精致。
创建精灵表动画
精灵表会将多帧图像按规则网格排列在一张图片中。使用帧索引在 AnimationManager 中定义动画。
// preload()
preload(): void {
this.load.spritesheet('player', 'assets/player.png', {
frameWidth: 48,
frameHeight: 48,
});
}
// create() — or PreloaderScene.create() for global animations (see below)
create(): void {
this.anims.create({
key: 'player-idle',
frames: this.anims.generateFrameNumbers('player', { start: 0, end: 3 }),
frameRate: 8,
repeat: -1, // -1 = loop forever
});
this.anims.create({
key: 'player-walk',
frames: this.anims.generateFrameNumbers('player', { start: 4, end: 11 }),
frameRate: 12,
repeat: -1,
});
this.anims.create({
key: 'player-jump',
frames: this.anims.generateFrameNumbers('player', { start: 12, end: 15 }),
frameRate: 10,
repeat: 0, // 0 = play once
});
this.anims.create({
key: 'player-attack',
frames: this.anims.generateFrameNumbers('player', { start: 16, end: 23 }),
frameRate: 16,
repeat: 0,
});
}
generateFrameNumbers 选项
this.anims.generateFrameNumbers('texture', {
start: 0, // first frame index
end: 7, // last frame index (inclusive)
first: 0, // override which frame plays first
frames: [0, 2, 4], // manual frame list (use instead of start/end)
});
基于图集的动画
纹理图集使用命名键而非网格位置存储帧。使用 generateFrameNames 生成这些帧:
// preload()
this.load.atlas('hero', 'assets/hero.png', 'assets/hero.json');
// create()
this.anims.create({
key: 'hero-run',
frames: this.anims.generateFrameNames('hero', {
prefix: 'run_', // frame names are run_01, run_02, ...
start: 1,
end: 8,
zeroPad: 2, // zero-pad the number to 2 digits
suffix: '', // optional suffix after the number
}),
frameRate: 12,
repeat: -1,
});
// Manual frame list from atlas
this.anims.create({
key: 'hero-die',
frames: [
{ key: 'hero', frame: 'die_01' },
{ key: 'hero', frame: 'die_02' },
{ key: 'hero', frame: 'die_03' },
],
frameRate: 8,
repeat: 0,
});
动画的定义位置
在 PreloaderScene.create() 中定义动画,而不要在各个场景中分别定义。 在全局 AnimationManager 中注册的动画可供所有场景使用,无需重复注册:
// src/scenes/PreloaderScene.ts
export class PreloaderScene extends Phaser.Scene {
preload(): void {
this.load.spritesheet('player', 'assets/player.png', { frameWidth: 48, frameHeight: 48 });
this.load.atlas('enemies', 'assets/enemies.png', 'assets/enemies.json');
}
create(): void {
// All anims defined here are available in GameScene, UIScene, etc.
this.anims.create({ key: 'player-idle', /* ... */ });
this.anims.create({ key: 'player-walk', /* ... */ });
this.anims.create({ key: 'enemy-walk', /* ... */ });
this.scene.start('GameScene');
}
}
如果某个动画只适用于单个场景(例如过场动画),则在该场景的 create() 中定义。
播放动画
// Basic play
sprite.play('player-walk');
// Play but don't restart if already playing this animation
sprite.play('player-walk', true); // ignoreIfPlaying = true
// Play starting from a specific frame
sprite.playFromFrame('player-walk', 3);
// Stop on a specific frame number
sprite.stopOnFrame(this.anims.get('player-attack').frames[7]);
// Reverse playback
sprite.playReverse('player-walk');
// Check state
sprite.anims.isPlaying; // boolean
sprite.anims.currentAnim?.key; // string | undefined
sprite.anims.currentFrame?.index; // current frame index
动画事件
在精灵上(而非 AnimationManager 上)监听动画生命周期事件:
// Fires when any animation completes on this sprite
sprite.on(Phaser.Animations.Events.ANIMATION_COMPLETE, (anim, frame, gameObject) => {
console.log('animation complete:', anim.key);
});
// Fires when a SPECIFIC animation completes (preferred — avoids key checks)
sprite.on(
Phaser.Animations.Events.ANIMATION_COMPLETE_KEY + 'player-attack',
(anim, frame, gameObject) => {
this.player.returnToIdle();
}
);
// Other events
sprite.on(Phaser.Animations.Events.ANIMATION_START, cb); // animation started
sprite.on(Phaser.Animations.Events.ANIMATION_REPEAT, cb); // loop restarted
sprite.on(Phaser.Animations.Events.ANIMATION_RESTART, cb); // play() called while already playing
sprite.on(Phaser.Animations.Events.ANIMATION_STOP, cb); // stop() called
sprite.on(Phaser.Animations.Events.ANIMATION_UPDATE, cb); // every frame change
精灵销毁时,务必移除监听器,避免内存泄漏:
sprite.on(Phaser.Animations.Events.ANIMATION_COMPLETE, this.onAnimComplete, this);
// In shutdown():
sprite.off(Phaser.Animations.Events.ANIMATION_COMPLETE, this.onAnimComplete, this);
动画衔接
依次播放一系列动画:
// Chain via array — plays 'attack', then 'idle' automatically
sprite.chain(['player-attack', 'player-idle']);
sprite.play('player-attack');
// Chain via ANIMATION_COMPLETE event
sprite.play('player-jump');
sprite.once(
Phaser.Animations.Events.ANIMATION_COMPLETE_KEY + 'player-jump',
() => sprite.play('player-fall')
);
角色状态机模式
对于具有 idle/walk/jump/attack 状态的角色,在 update() 中使用显式状态机。这样可以避免不可能的状态转换,也能让动画逻辑更易读。
type CharState = 'idle' | 'walk' | 'jump' | 'attack' | 'hurt';
export class Player extends Phaser.Physics.Arcade.Sprite {
private state: CharState = 'idle';
setState(newState: CharState): void {
if (this.state === newState) return;
this.state = newState;
switch (newState) {
case 'idle': this.play('player-idle', true); break;
case 'walk': this.play('player-walk', true); break;
case 'jump': this.play('player-jump', true); break;
case 'attack': this.play('player-attack', true); break;
case 'hurt':
this.play('player-hurt', true);
this.once(
Phaser.Animations.Events.ANIMATION_COMPLETE_KEY + 'player-hurt',
() => this.setState('idle')
);
break;
}
}
update(cursors: Phaser.Types.Input.Keyboard.CursorKeys): void {
const body = this.body as Phaser.Physics.Arcade.Body;
if (this.state === 'attack' || this.state === 'hurt') return; // locked states
if (!body.blocked.down) {
this.setState('jump');
} else if (cursors.left.isDown || cursors.right.isDown) {
this.setState('walk');
} else {
this.setState('idle');
}
if (Phaser.Input.Keyboard.JustDown(cursors.space)) {
this.setState('attack');
this.once(
Phaser.Animations.Events.ANIMATION_COMPLETE_KEY + 'player-attack',
() => this.setState('idle')
);
}
}
}
强制播放动画:电影模式
当播放单次动画(Boss 登场、死亡序列、进入地牢等)时,如果动画只播放一帧就恢复为 idle,原因总是实体的 update() 在你强制调用 play() 后的一个时间步内运行了状态机逻辑,并覆盖了该动画。
修复方法——在 update() 中添加 cinematicMode 标志,并将其作为最先执行的守卫条件:
export class Player extends Phaser.Physics.Arcade.Sprite {
private state: CharState = 'idle';
private cinematicMode = false;
setCinematicMode(active: boolean, forcedAnimKey?: string): void {
this.cinematicMode = active;
if (active && forcedAnimKey) {
this.anims.stop(); // always stop before play on a state switch
this.play(forcedAnimKey, true);
this.once(
Phaser.Animations.Events.ANIMATION_COMPLETE_KEY + forcedAnimKey,
() => { this.cinematicMode = false; }
);
}
}
update(cursors: Phaser.Types.Input.Keyboard.CursorKeys): void {
if (this.cinematicMode) return; // MUST be first line — blocks state logic
// ... rest of state machine
}
}
在 ANIMATION_COMPLETE_KEY 处理程序中清除 cinematicMode,不要在 play() 后立即同步清除——完成事件会在最后一帧渲染后触发,而 update() 可能会在处理程序执行前运行。如果同步清除,该强制动画会提前一帧结束。
参阅 references/state-machine-patterns.md,了解完整的标准实现、地牢入口示例,以及 ANIMATION_COMPLETE 时序修复方法。
状态转换完整性
添加新的动画状态却不检查所有其他状态的转换列表,是一种不易察觉的错误——不会报错;状态机只会无法进入新状态,或卡在错误状态中。
添加任何新状态(例如 'dodge'、'interact')时:
- 将其添加到
CharState联合类型中。 - 在
setState()中为其添加一个case。 - 更新其他每个状态中“哪些状态可以中断我”的逻辑,按需加入或排除这个新状态。
references/state-machine-patterns.md 中的转换表能让遗漏的转换一目了然。
停止和暂停动画
sprite.stop(); // stop and stay on current frame
sprite.anims.pause(); // pause on current frame (resumable)
sprite.anims.resume(); // resume paused animation
sprite.anims.restart(); // restart from frame 0
补间
补间会随时间对任意对象上的任意数值属性进行插值。它是 Phaser 用于 UI 动画、过场动画和视觉反馈的主要工具。
this.tweens.add({
targets: sprite, // one object, an array, or a group
x: 400, // tween x to 400
y: 300,
alpha: 1,
duration: 800, // milliseconds
ease: 'Quad.Out', // easing function
delay: 0, // ms before starting
repeat: 0, // 0 = once; -1 = infinite
yoyo: false, // reverse back to start after completing
hold: 0, // ms to hold at end before yoyo
onStart: () => {}, // fires when tween starts
onUpdate: () => {}, // fires every frame
onComplete: () => {}, // fires on completion
});
常见补间模式
淡入
sprite.setAlpha(0);
this.tweens.add({ targets: sprite, alpha: 1, duration: 400, ease: 'Linear' });
淡出并销毁
this.tweens.add({
targets: sprite,
alpha: 0,
duration: 300,
ease: 'Linear',
onComplete: () => sprite.destroy(),
});
缩放脉冲(受击反馈、可收集物品)
this.tweens.add({
targets: sprite,
scaleX: 1.3,
scaleY: 1.3,
duration: 80,
ease: 'Quad.Out',
yoyo: true,
});
从边缘滑入
// Slide in from left
sprite.setX(-100);
this.tweens.add({
targets: sprite,
x: 400,
duration: 500,
ease: 'Back.Out',
});
弹跳落地
sprite.setY(targetY - 100);
this.tweens.add({
targets: sprite,
y: targetY,
duration: 600,
ease: 'Bounce.Out',
});
补间缓动函数
完整指南(包含所有缓动函数及其适用场景)请参阅 references/easing-reference.md。
速查:
'Linear'— 匀速;适用于机械运动、UI 进度条'Quad.Out'— 起步快,随后减速;最自然的运动效果'Quad.In'— 逐渐加速;适用于下落物体、蓄力动作'Quad.InOut'— 对称缓动;适用于镜头移动'Back.Out'— 越过目标后回弹至稳定位置;适用于 UI 弹窗、对话框滑入'Bounce.Out'— 在终点弹跳;适用于物体落地'Elastic.Out'— 弹簧振荡;适用于卡通风格、弹跳感的 UI
补间动画时间线
无需嵌套 onComplete 回调即可串联多个补间动画:
this.tweens.timeline({
tweens: [
{
targets: panel,
alpha: 1,
duration: 200,
},
{
targets: panel,
y: 300,
duration: 400,
ease: 'Back.Out',
},
{
targets: title,
alpha: 1,
duration: 300,
offset: '-=100', // start 100ms before previous tween ends (overlap)
},
{
targets: button,
alpha: 1,
duration: 200,
// no offset = starts after previous completes
},
],
});
offset 控制相对于前一个补间动画的时间:
'-=200'— 重叠 200ms'+=200'— 增加 200ms 间隔- 绝对数值 — 从时间线开始处的该毫秒数开始
粒子动画(简要)
对于爆发效果(爆炸、拾取、撞击),请使用内置粒子系统:
// One-shot burst
this.add.particles(x, y, 'spark', {
speed: { min: 50, max: 200 },
angle: { min: 0, max: 360 },
scale: { start: 1, end: 0 },
lifespan: 600,
quantity: 12,
emitting: false, // don't start automatically
}).explode(12); // emit 12 particles immediately then stop
// Persistent emitter (fire, rain)
const emitter = this.add.particles(x, y, 'flame', {
speed: 30,
lifespan: 1200,
scale: { start: 0.8, end: 0 },
alpha: { start: 1, end: 0 },
frequency: 80, // ms between emissions
});
// Stop later:
emitter.stop();
更多资源
参考文件
references/animation-api.md— AnimationManager、AnimationConfig、Animation 事件、TweenManager 和 Timeline API 的完整参考references/easing-reference.md— 所有内置缓动函数的说明、适用场景和代码示例references/state-machine-patterns.md— 角色状态机规范:cinematicMode标志、规范状态列表、转换表、stop()/play()顺序规则,以及ANIMATION_COMPLETE的时序。构建包含 idle+walk 以外状态的角色,或强制播放的动画只播放一帧便恢复时,请阅读此文档。