Scaling is deforming sprite from atlas

Hi. I have a tilemap with a 32 x 32 tile size and an atlas for a 32 x 41 player sprite that looks like this:

atlas

The problem (I think) is that Phaser is scaling the sprite differently each time it shows the animations, and deforming it in different ways each time. For instance:

animation-deformed

These are screenshots of the same animation frame (running in Chrome). You can see that they are a bit different each time. This is especially notable in the eyes.

Changing the scale mode from RESIZE to NONE (or FIT / ENVELOP for that matter) does nothing to fix the issue. Neither does resizing the sprite to 32 x 32.

The atlas is being loaded with:

preload() {
...
this.load.atlas("atlas", "./assets/prod/atlas/atlas.png", "./assets/prod/atlas/atlas.json");
...
}

And in my create function I have:

this.player = this.physics.add
  .sprite(spawnPoint.x, spawnPoint.y, "atlas", "ariel-front")
  .setSize(32, 41);

const anims = this.anims;
anims.create({
  key: "ariel-left-walk",
  frames: anims.generateFrameNames("atlas", {
    prefix: "ariel-left-walk.",
    start: 0,
    end: 3,
    zeroPad: 3
  }),
  frameRate: 10,
  repeat: -1
});
anims.create({
  key: "ariel-right-walk",
  frames: anims.generateFrameNames("atlas", {
    prefix: "ariel-right-walk.",
    start: 0,
    end: 3,
    zeroPad: 3
  }),
  frameRate: 10,
  repeat: -1
});
// etc for the rest of the animations...

Is there any way around this?
Thanks in advance

I would avoid scaling the sprite at all.

Turn on pixelArt in the game config.

pixelArt is on. Here’s my current game config:

const config = {
  type: Phaser.AUTO,
  scale: {
    // mode: Phaser.Scale.NONE,
    mode: Phaser.Scale.RESIZE,
    autoCenter: Phaser.Scale.CENTER_BOTH,
    parent: "game-container",
  },
  parent: "game-container",
  pixelArt: true,
  physics: {
    default: "arcade",
    arcade: {
      debug: true,  // Remove in production
      // debug: false,
      gravity: { y: 0 }
    }
  },
  scene: [SceneA, SceneB, SceneC]
};

const game = new Phaser.Game(config);

Try with Phaser.Scale.FIT

It’s the same. I tried Phaser.Scale.FIT, Phaser.Scale.RESIZE, Phaser.Scale.NONE and Phaser.Scale.ENVELOP. All result in the same problem.

So the problem isn’t related to Phaser.Scale, put it to NONE until you find the problem.
Paste your atlas.json here (just in case)
What is the size of your canvas in phaser config ?
I usually use this config:

const config = {
  type: Phaser.AUTO,
  width: 640,
  height: 480,
  pixelArt: true,
  scale: {
    parent: 'game-container',
    mode: Phaser.Scale.HEIGHT_CONTROLS_WIDTH,
    autoRound: true,
    autoCenter: Phaser.DOM.CENTER_BOTH,
  },
  physics: {
    default: 'arcade',
    arcade: {
      tileBias: 20, // for 16x16 tile size
      gravity: { y: 600 }, 
      debug: true,
      debugShowBody: true,
      debugShowStaticBody: true,
    },
  },
  scene: [SceneA, SceneB, SceneC],
};

Remove setSize(32, 41) and any other sprite scaling.

Does the Phaser banner in the console show Canvas or WebGL?

Ok, so I tried:

  • Setting the scale mode to NONE
  • Explicitly adding a height and width of 640 x 480 to the game config
  • Adding autoRound:true
  • Removing setSize

The problem persists :sweat:
The console shows Phaser v3.54.0 (WebGL | Web Audio) https://phaser.io

Here’s my full atlas.json:

{"frames": {

"ariel-front":
{
	"frame": {"x":32,"y":0,"w":32,"h":41},
	"rotated": false,
	"trimmed": false,
	"spriteSourceSize": {"x":0,"y":0,"w":32,"h":41},
	"sourceSize": {"w":32,"h":41}
},
"ariel-front-walk.000":
{
	"frame": {"x":0,"y":0,"w":32,"h":41},
	"rotated": false,
	"trimmed": false,
	"spriteSourceSize": {"x":0,"y":0,"w":32,"h":41},
	"sourceSize": {"w":32,"h":41}
},
"ariel-front-walk.001":
{
	"frame": {"x":32,"y":0,"w":32,"h":41},
	"rotated": false,
	"trimmed": false,
	"spriteSourceSize": {"x":0,"y":0,"w":32,"h":41},
	"sourceSize": {"w":32,"h":41}
},
"ariel-front-walk.002":
{
	"frame": {"x":64,"y":0,"w":32,"h":41},
	"rotated": false,
	"trimmed": false,
	"spriteSourceSize": {"x":0,"y":0,"w":32,"h":41},
	"sourceSize": {"w":32,"h":41}
},
"ariel-front-walk.003":
{
	"frame": {"x":32,"y":0,"w":32,"h":41},
	"rotated": false,
	"trimmed": false,
	"spriteSourceSize": {"x":0,"y":0,"w":32,"h":41},
	"sourceSize": {"w":32,"h":41}
},
"ariel-back":
{
	"frame": {"x":96,"y":41,"w":32,"h":41},
	"rotated": false,
	"trimmed": false,
	"spriteSourceSize": {"x":0,"y":0,"w":32,"h":41},
	"sourceSize": {"w":32,"h":41}
},
"ariel-back-walk.000":
{
	"frame": {"x":96,"y":0,"w":32,"h":41},
	"rotated": false,
	"trimmed": false,
	"spriteSourceSize": {"x":0,"y":0,"w":32,"h":41},
	"sourceSize": {"w":32,"h":41}
},
"ariel-back-walk.001":
{
	"frame": {"x":96,"y":41,"w":32,"h":41},
	"rotated": false,
	"trimmed": false,
	"spriteSourceSize": {"x":0,"y":0,"w":32,"h":41},
	"sourceSize": {"w":32,"h":41}
},
"ariel-back-walk.002":
{
	"frame": {"x":128,"y":0,"w":32,"h":41},
	"rotated": false,
	"trimmed": false,
	"spriteSourceSize": {"x":0,"y":0,"w":32,"h":41},
	"sourceSize": {"w":32,"h":41}
},
"ariel-back-walk.003":
{
	"frame": {"x":96,"y":41,"w":32,"h":41},
	"rotated": false,
	"trimmed": false,
	"spriteSourceSize": {"x":0,"y":0,"w":32,"h":41},
	"sourceSize": {"w":32,"h":41}
},
"ariel-left":
{
	"frame": {"x":128,"y":41,"w":32,"h":41},
	"rotated": false,
	"trimmed": false,
	"spriteSourceSize": {"x":0,"y":0,"w":32,"h":41},
	"sourceSize": {"w":32,"h":41}
},
"ariel-left-walk.000":
{
	"frame": {"x":0,"y":41,"w":32,"h":41},
	"rotated": false,
	"trimmed": false,
	"spriteSourceSize": {"x":0,"y":0,"w":32,"h":41},
	"sourceSize": {"w":32,"h":41}
},
"ariel-left-walk.001":
{
	"frame": {"x":128,"y":41,"w":32,"h":41},
	"rotated": false,
	"trimmed": false,
	"spriteSourceSize": {"x":0,"y":0,"w":32,"h":41},
	"sourceSize": {"w":32,"h":41}
},
"ariel-left-walk.002":
{
	"frame": {"x":32,"y":41,"w":32,"h":41},
	"rotated": false,
	"trimmed": false,
	"spriteSourceSize": {"x":0,"y":0,"w":32,"h":41},
	"sourceSize": {"w":32,"h":41}
},
"ariel-left-walk.003":
{
	"frame": {"x":128,"y":41,"w":32,"h":41},
	"rotated": false,
	"trimmed": false,
	"spriteSourceSize": {"x":0,"y":0,"w":32,"h":41},
	"sourceSize": {"w":32,"h":41}
},
"ariel-right":
{
	"frame": {"x":160,"y":41,"w":32,"h":41},
	"rotated": false,
	"trimmed": false,
	"spriteSourceSize": {"x":0,"y":0,"w":32,"h":41},
	"sourceSize": {"w":32,"h":41}
},
"ariel-right-walk.000":
{
	"frame": {"x":64,"y":41,"w":32,"h":41},
	"rotated": false,
	"trimmed": false,
	"spriteSourceSize": {"x":0,"y":0,"w":32,"h":41},
	"sourceSize": {"w":32,"h":64}
},
"ariel-right-walk.001":
{
	"frame": {"x":160,"y":41,"w":32,"h":41},
	"rotated": false,
	"trimmed": false,
	"spriteSourceSize": {"x":0,"y":0,"w":32,"h":41},
	"sourceSize": {"w":32,"h":41}
},
"ariel-right-walk.002":
{
	"frame": {"x":160,"y":0,"w":32,"h":41},
	"rotated": false,
	"trimmed": false,
	"spriteSourceSize": {"x":0,"y":0,"w":32,"h":41},
	"sourceSize": {"w":32,"h":41}
},
"ariel-right-walk.003":
{
	"frame": {"x":160,"y":41,"w":32,"h":41},
	"rotated": false,
	"trimmed": false,
	"spriteSourceSize": {"x":0,"y":0,"w":32,"h":41},
	"sourceSize": {"w":32,"h":41}
}
},
"meta": {
	"image": "atlas.png",
	"format": "RGBA8888",
	"size": {"w":192,"h":82},
	"scale": "1"
}

}

Using scale mode Phaser.Scale.HEIGHT_CONTROLS_WIDTH makes it less noticeable, but it still does persist as well

There should be no distortion here. pixelArt is on? The sprite scale isn’t changed at all?

Ok, I think I managed to pinpoint the issue. I went back to basics and created simple test example. I made a new tileset consisting only of a single 32 x 32 tile, which is this:

grass_tile

And a new 2 tile x 2 tile tilemap in Tiled that has just 4 copies of that tile in a single layer. I used the same atlas as before, which has a 32 x 41 sprite.

The HTML file:

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Test</title>
  </head>
  <body>
    <div id="game-container"></div>
    <script src="//cdn.jsdelivr.net/npm/phaser@3.54.0/dist/phaser.js"></script>
    <script src="./index.js" type="module"></script>
  </body>
</html>

The index.js file:

class SceneA extends Phaser.Scene {
  constructor() {
    super("SceneA");
  }
  
  preload() {
	this.load.image("tileset", "./assets/test/grass_tile.png");
    this.load.tilemapTiledJSON("tilemap", "./assets/test/grass.json");
    this.load.atlas("atlas", "./assets/prod/atlas/atlas.png", "./assets/prod/atlas/atlas.json");
  }
  
  create() {
  this.map = this.make.tilemap({key: "tilemap"});
  const tileset = this.map.addTilesetImage("test_tileset", "tileset");
  const groundLayer = this.map.createLayer("Layer1", tileset, 0, 0);
  this.player = this.physics.add.sprite(31, 38, "atlas", "ariel-front");
  }
}

const config = {
  type: Phaser.AUTO,
  pixelArt: true,
  scale: {
    mode: Phaser.Scale.NONE,
    autoRound: true,
    autoCenter: Phaser.Scale.NO_CENTER,
    zoom: Phaser.Scale.NO_ZOOM,
    parent: "game-container",
    width: 64,
    height: 64,
  },
  physics: {
    default: "arcade",
    arcade: {
      debug: false,
      gravity: { y: 0 }
    }
  },
  scene: [SceneA]
};

const game = new Phaser.Game(config);

This is what I get:

bug2

Notice that the sprite is still deformed. BUT, i also noticed that the map square is not 64 x 64 screen pixels, but 80 x 80 screen pixels. Moreover, if I query window.devicePixelRatio in the console, it returns 1.25 (64 x 1.25 = 80). So I assume that what is happening is that Phaser is sizing the map to 64 x 64 CSS pixels, which forces it to scale the sprite and thus deform it.

I tried adding:

<meta name="viewport" content="initial-scale=1, maximum-scale=1"/>

to the HTML head but this doesn’t seem to fix the issue.

Any tips on how to solve this?
Thanks!

Reset the page zoom.

Ok, adding a script with document.body.style.zoom = 1 / window.devicePixelRatio; fixed the zooming issue.

But now I have a new problem, which is that pointer clicks are being registered in the wrong x and y coordinates.

update(time, delta) {
  // ...
  let pointer = this.input.activePointer;
  if (pointer.primaryDown) {
    let pointerPosition = this.cameras.main.getWorldPoint(pointer.x, pointer.y);
    console.log(Math.floor(this.player.y), Math.floor(pointer.position.y), Math.floor(pointerPosition.y));
  }
  // ...
}

Clicking on the player prints, for example, 1061 621 915

I mean in the Chrome View menu, do Actual Size.

Don’t change body.style.

But the zoom in Chrome was already set to 100%. I can manually change it to 80% and it will look ok, but that is not a solution, I cannot ask visitors to my site to adjust their browser zoom. The problem happens in Firefox as well. And in mobile devices the CSS pixel vs screen pixel ratio will almost never be 1, which wil make Phaser scale the game as well.

If you need to handle DPR you can add { zoom: 1 / devicePixelRatio }.

This works for me, but not with scale mode set to Phaser.Scale.RESIZE. Since I want the game to cover the entire screen, I ended up doing:

{
  mode: Phaser.Scale.NONE,
  width: window.innerWidth * window.devicePixelRatio,
  height: window.innerHeight * window.devicePixelRatio,
  zoom: 1 / window.devicePixelRatio,
}

I’ll also have to resize the game each time the screen is resized. Do you think this is acceptable?
Thank you for all your help!!! :blush: