Add new tile to tileset during 'create' phase

Hi everyone,

I’m relatively new to Phaser and working on my first project. I’m trying to allow users to draw sketches in the game, which will then become part of the game terrain or item. To achieve this, I’ve created a “sketchpad” where users can draw, and I’m attempting to merge the current tileset with the new sketch.

Here’s a simplified overview of what I’m doing:

  1. The user draws a sketch in the game.
  2. I convert the sketch to a base64 image.
  3. I merge this image with the existing tileset using canvas operations.
  4. I add this new tileset to Phaser using textures.addBase64().
  5. I update the Tileset data using tileset.setImage().
  6. Finally, I attempt to place the new tile using groundLayer.putTileAt().

However, I’m encountering an error at the last step. Despite seeing that the Tileset has been updated correctly, I’m getting the following error:

phaser.esm.js:223298 Uncaught TypeError: Cannot read properties of undefined (reading '2')
    at Object.PutTileAt (phaser.esm.js:223298:25)
    at TilemapLayer2.putTileAt (phaser.esm.js:219405:34)
    at TextureManager2.<anonymous> (Game.ts:109:32)
    at TextureManager2.emit (phaser.esm.js:187:35)
    at image.onload (phaser.esm.js:211023:23)

And where should render the new tile, it’s rendering anything, a transparent space.

I’ve read the Phaser documentation and searched the forum but couldn’t find a solution. If anyone has experience with this or can suggest an alternative approach, I’d greatly appreciate your help.

Here’s the relevant code from my scene for reference:

export class GameScene extends Phaser.Scene {
  private map!: Phaser.Tilemaps.Tilemap;
  private tileset!: Phaser.Tilemaps.Tileset;
  private groundLayer!: Phaser.Tilemaps.TilemapLayer;

  constructor() {
    super({ key: 'GameScene', active: true });
  }

  preload() {
    this.load.image('tileset', 'assets/Terrains/tileset.png');
  }

  create() {
    const mapWidth = 25 * 40;
    const mapHeight = 19 * 40;
    const tileSize = 16;

    this.map = this.scene.scene.make.tilemap({ key: 'map', width: mapWidth, height: mapHeight, tileWidth: tileSize, tileHeight: tileSize });
    this.tileset = this.map.addTilesetImage('tileset')!;
    this.groundLayer = gameTable.groundLayer = this.map.createBlankLayer('layer1', this.tileset, 0, 0) as Phaser.Tilemaps.TilemapLayer;

    generateTerrainMap(this.map, this.groundLayer); // just populate the layer using a noise and putTIleAt();

    if (this.input.mouse) {
      this.input.mouse.disableContextMenu();
    }

    this.setupInputListeners();
  }

  processSketch(params: { imageData: string) {
    const canvas = document.createElement('canvas');
    const context: any = canvas.getContext('2d');

    const generatedImage = new Image();
    generatedImage.src = imageData; // base64, 512x512 px

    generatedImage.onload = () => {
      const targetWidth = 16;
      const targetHeight = 16;

      // resize sketch
      canvas.width = targetWidth;
      canvas.height = targetHeight;
      context.drawImage(generatedImage, 0, 0, targetWidth, targetHeight);
      const resizedBase64 = canvas.toDataURL('image/png');
      const resizedImage = new Image();
      resizedImage.src = resizedBase64;

      resizedImage.onload = () => {
        console.log(this.map.getTileset('tileset'))
        console.log(this.tileset.total);

        let tilesetBase64 = this.textures.getBase64('tileset');
        console.log(tilesetBase64);
        const tilesetImage= new Image();
        tilesetImage.src = tilesetBase64;

        tilesetImage.onload = () => {
          console.log('original', tilesetImage.width, tilesetImage.height);
          console.log('new', resizedImage.width, resizedImage.height);
          
          // clear canvas and resize to new tileset size
          context.clearRect(0, 0, canvas.width, canvas.height);
          canvas.width = tilesetImage.width + targetWidth;
          canvas.height = Math.max(tilesetImage.height, targetHeight);
          
          // draw the new tileset appending the new tile
          context.drawImage(tilesetImage, 0, 0);
          context.drawImage(resizedImage, tilesetImage.width, 0);

          const mergedBase64Image = canvas.toDataURL('image/png');
          console.log(mergedBase64Image); // we are getting the correct image

          const mergedImage = new Image();
          mergedImage.src = mergedBase64Image;

          mergedImage.onload = () => {
            this.textures.addBase64('mergedTexture', mergedBase64Image);

            this.textures.on('onload', () => {
              let mergedTexture = this.scene.systems.textures.get('mergedTexture');
              console.log(mergedTexture);
              
              this.tileset.setImage(mergedTexture);

              console.log(this.tileset.total); // here we are getting the correct value, the old tileset total + 1
              let tilesetLen = this.tileset.total;

              this.groundLayer.putTileAt(tilesetLen - 1, 0, 0); // the error happens here
            });
          };
        }
      };
    }
  }

Thank you in advance for your assistance. Please let me know if there’s any additional information I can provide.

Edit: remove ‘async’ from function and add description of transparent ‘tile’ after error

:wave:

After setImage() add

Phaser.Tilemaps.Parsers.Tiled.BuildTilesetIndex(this.map);

I believe you could also use a CanvasTexure as the tileset texture and draw on that directly without copying or resizing.

1 Like

Hi samme,

Thank you for your earlier suggestions; they got me closer to a solution, but I’m still facing some challenges.

Using BuildTilesetIndex

I tried to use BuildTilesetIndex, but I’m uncertain about how to obtain the MapData from the Tilemap to use in the call since this.map is a Tilemap. Here’s what I’ve attempted:

...
            this.textures.addBase64('mergedTexture', mergedBase64Image);

            this.textures.on('onload', () => {
              let mergedTexture = this.scene.systems.textures.get('mergedTexture');
              this.tileset.setImage(mergedTexture);

              const mapDataInstance = new Phaser.Tilemaps.MapData({
                imageCollections: this.map.imageCollections,
                tilesets: this.map.tilesets,
              });

              // Call BuildTilesetIndex with the MapData instance
              const tilesetIndex = Phaser.Tilemaps.Parsers.Tiled.BuildTilesetIndex(mapDataInstance);

              this.groundLayer.putTileAt(tilesetLen - 1, 0, 0);
            }
...

However, I’m still encountering the same behavior: the tile at (0, 0) becomes transparent, and I get the error Uncaught TypeError: Cannot read properties of undefined (reading '2').

CanvasTexture

I also attempted to update the CanvasTexture while using it as a tileset image. Here’s the code snippet:

create() {
    const mapWidth = 25 * 40;
    const mapHeight = 19 * 40;
    const tileSize = 16;

    this.map = this.scene.scene.make.tilemap({ key: 'map', width: mapWidth, height: mapHeight, tileWidth: tileSize, tileHeight: tileSize });

    this.tilesetCanvas = document.createElement('canvas');

    const tilesetImageElement = new Image();
    tilesetImageElement.src = this.textures.getBase64('tileset');

    tilesetImageElement.onload = () => {
      this.tilesetCanvas.width = tilesetImageElement.width;
      this.tilesetCanvas.height = tilesetImageElement.height;

      this.tilesetCanvasTexture = this.textures.createCanvas('atlas', tilesetImageElement.width, tilesetImageElement.height)!;
      const ctx = this.tilesetCanvasTexture.context;

      ctx.drawImage(tilesetImageElement, 0, 0, tilesetImageElement.width, tilesetImageElement.height);
      this.tilesetCanvasTexture.refresh()

      console.log(tilesetImageElement.src)

      this.tileset = this.map.addTilesetImage('atlas')!;
      console.log(this.tileset);
      console.log(this.tilesetCanvasTexture);

      this.groundLayer = gameTable.groundLayer = this.map.createBlankLayer('layer1', this.tileset, 0, 0) as Phaser.Tilemaps.TilemapLayer;

      generateTerrainMap(this.map, this.groundLayer);
}

...

processSketch(params: { imageData: string) {
    const generatedImage = new Image();
    generatedImage.src = imageData; // base64, 512x512 px

    generatedImage.onload = () => {
      const targetWidth = 16;
      const targetHeight = 16;

      const utilCanvas = document.createElement('canvas');
      const context: any = utilCanvas.getContext('2d');

      utilCanvas.width = targetWidth;
      utilCanvas.height = targetHeight;

      context.drawImage(generatedImage, 0, 0, targetWidth, targetHeight);
      const resizedBase64 = utilCanvas.toDataURL('image/png');
      const resizedImage = new Image();
      resizedImage.src = resizedBase64;

      resizedImage.onload = () => {
        console.log(resizedImage.src);

        const originalTileset = new Image();
        originalTileset.src = this.tilesetCanvasTexture.canvas.toDataURL('image/png');

        originalTileset.onload = () => {
          console.log(originalTileset.src);

          const tilesetCanvas = this.tilesetCanvasTexture.canvas;
          const tilesetCtx = this.tilesetCanvasTexture.context;

          tilesetCanvas.width = tilesetCanvas.width + targetWidth;
          tilesetCanvas.height = tilesetCanvas.height;

          tilesetCtx.drawImage(originalTileset, 0, 0);
          tilesetCtx.drawImage(resizedImage, originalTileset.width, 0);
          console.log(tilesetCtx.canvas.toDataURL('image/png')); // good

          this.tilesetCanvas = this,this.tilesetCanvasTexture.canvas;
          this.tilesetCanvasTexture.refresh();

          this.groundLayer.putTileAt(8, 0, 0); // simplified, original tileset max index is 7
        }
      };
    }

Despite these attempts, I’m still facing issues. After calling processSketch, all the tiles on the map turn black, and the (0, 0) tile becomes transparent. Additionally, the tileset.total value remains 8, indicating that our new tile hasn’t been properly added.

Any further guidance or suggestions would be greatly appreciated. Thank you!

The “mapData” argument is really the tilemap itself, in this case. All you need is

Phaser.Tilemaps.Parsers.Tiled.BuildTilesetIndex(this.map);

I’ll have to look at the sketch code some more. But I’d guess you don’t need this at all:

Typescript don’t let me use this.map as parameter:

Argument of type 'Tilemap' is not assignable to parameter of type 'MapData'.
  Type 'Tilemap' is missing the following properties from type 'MapData': name, infinite, collision, staggerAxis, staggerIndexts(2345)

I’m using phaser 3.60.0

Ok, I will upload a minimal replication as soon as possible.

Phaser’s type isn’t exactly correct. I think you have to use this.map as Phaser.Tilemaps.MapData.

The CanvasTexture can work like this:

1 Like