- Updated the game to support new character sprite atlases for both male and female characters, allowing for a wider variety of NPC designs. - Improved player sprite initialization to dynamically select between atlas-based and legacy sprites, enhancing flexibility in character representation. - Refined collision box settings based on sprite type, ensuring accurate physics interactions for both atlas (80x80) and legacy (64x64) sprites. - Enhanced NPC behavior to utilize atlas animations, allowing for more fluid and diverse animations based on available frames. Files modified: - game.js: Added new character atlases and updated sprite loading logic. - player.js: Improved player sprite handling and collision box adjustments. - npc-behavior.js: Updated animation handling for NPCs to support atlas-based animations. - npc-sprites.js: Enhanced NPC sprite creation to accommodate atlas detection and initial frame selection. - scenario.json.erb: Updated player and NPC configurations to utilize new sprite sheets and animation settings. - m01_npc_sarah.ink: Revised dialogue options to include new interactions related to NPCs.
6.0 KiB
NPC Animation Fix - Single Frame Issue
Problem
NPCs were stuck on a single frame and not playing any animations, appearing completely static even when they should have been playing breathing-idle or walk animations.
Root Cause
The code was checking sprite.anims.exists(animKey) instead of scene.anims.exists(animKey).
The Bug
In Phaser 3:
scene.anims- The scene's animation manager (where animations are registered globally)sprite.anims- The sprite's animation component (handles playing animations on that sprite)
The bug was using sprite.anims.exists() which checks if an animation is currently assigned to the sprite, not whether the animation exists in the animation manager.
Affected Code Locations
createNPCSprite()- Initial animation not playingplayNPCAnimation()- Helper function not finding animationsreturnNPCToIdle()- NPCs not returning to idle
Solution
Changed all animation existence checks from sprite.anims.exists() to scene.anims.exists().
Location 1: NPC Creation (createNPCSprite)
Before:
const idleAnimKey = `npc-${npc.id}-idle`;
if (sprite.anims.exists(idleAnimKey)) { // ❌ WRONG
sprite.play(idleAnimKey, true);
}
After:
const idleAnimKey = `npc-${npc.id}-idle`;
if (scene.anims.exists(idleAnimKey)) { // ✅ CORRECT
sprite.play(idleAnimKey, true);
}
Location 2: Play Animation Helper (playNPCAnimation)
Before:
export function playNPCAnimation(sprite, animKey) {
if (!sprite || !sprite.anims) {
return false;
}
if (sprite.anims.exists(animKey)) { // ❌ WRONG
sprite.play(animKey);
return true;
}
return false;
}
After:
export function playNPCAnimation(sprite, animKey) {
if (!sprite || !sprite.anims || !sprite.scene) {
return false;
}
if (sprite.scene.anims.exists(animKey)) { // ✅ CORRECT
sprite.play(animKey);
return true;
}
return false;
}
Location 3: Return to Idle (returnNPCToIdle)
Before:
export function returnNPCToIdle(sprite, npcId) {
if (!sprite) return;
const idleKey = `npc-${npcId}-idle`;
if (sprite.anims.exists(idleKey)) { // ❌ WRONG
sprite.play(idleKey, true);
}
}
After:
export function returnNPCToIdle(sprite, npcId) {
if (!sprite || !sprite.scene) return;
const idleKey = `npc-${npcId}-idle`;
if (sprite.scene.anims.exists(idleKey)) { // ✅ CORRECT
sprite.play(idleKey, true);
}
}
Phaser 3 Animation Architecture
Scene Animation Manager (scene.anims)
- Purpose: Global repository of animation definitions
- Scope: All sprites in the scene can use these animations
- Created by:
scene.anims.create() - Checked by:
scene.anims.exists(key)
// Create animation in scene
scene.anims.create({
key: 'npc-sarah-idle-down',
frames: [...],
frameRate: 6,
repeat: -1
});
// Check if animation exists
if (scene.anims.exists('npc-sarah-idle-down')) {
// Animation is registered
}
Sprite Animation Component (sprite.anims)
- Purpose: Controls playback on individual sprite
- Scope: Only affects this specific sprite
- Methods:
play(),stop(),pause(),resume() - Properties:
currentAnim,isPlaying,frameRate
// Play animation on sprite
sprite.play('npc-sarah-idle-down');
// Check what's currently playing
if (sprite.anims.isPlaying) {
console.log(sprite.anims.currentAnim.key);
}
Impact
Before Fix
❌ NPCs appeared completely frozen
❌ No breathing animation
❌ No walk animation during patrol
❌ No directional facing
❌ Looked like static images
After Fix
✅ NPCs play breathing-idle animation
✅ Walk animations work during patrol
✅ Proper 8-directional animations
✅ Smooth animation transitions
✅ Characters look alive and polished
Testing
Verified across all NPC behaviors:
- ✅ Initial idle animation on spawn
- ✅ Walk animation during patrol
- ✅ Idle animation when standing still
- ✅ Face player animation
- ✅ Chase animation (hostile NPCs)
- ✅ Return to idle after movement
Why This Bug Was Subtle
- No console errors:
sprite.anims.exists()is a valid method, it just checks the wrong thing - Silent failure: The
ifcondition simply evaluated tofalse, so no animation played - Sprite still visible: The NPC appeared on screen, just frozen on first frame
- Misleading: The method name
exists()sounds like it checks if animation exists globally
Prevention
Code Review Checklist
- Animation checks use
scene.anims.exists()notsprite.anims.exists() - Sprite has access to scene (
sprite.scene) - Animation keys match exactly (case-sensitive)
- Animations are created before being played
Common Mistakes to Avoid
❌ Wrong:
if (sprite.anims.exists('idle')) { ... }
if (this.sprite.anims.exists('walk')) { ... }
✅ Correct:
if (scene.anims.exists('idle')) { ... }
if (this.scene.anims.exists('walk')) { ... }
if (sprite.scene.anims.exists('idle')) { ... }
Related Documentation
- Phaser 3 Animation Manager: https://photonstorm.github.io/phaser3-docs/Phaser.Animations.AnimationManager.html
- Phaser 3 Sprite Animation: https://photonstorm.github.io/phaser3-docs/Phaser.GameObjects.Components.Animation.html
Files Modified
public/break_escape/js/systems/npc-sprites.jscreateNPCSprite()- Line 65playNPCAnimation()- Line 483returnNPCToIdle()- Line 501
Commit Message
Fix NPC animations stuck on single frame
NPCs were not playing any animations due to incorrect animation
existence checks. Changed from sprite.anims.exists() to
scene.anims.exists() in three locations:
- createNPCSprite() - Initial idle animation
- playNPCAnimation() - Helper function
- returnNPCToIdle() - Return to idle state
Now NPCs properly play breathing-idle and walk animations.