Search code examples
javascriptphaser-frameworkphaserjs

Phaser 3 - tilemap.createBlankLayer -> putTileAt issues when using multiple tilesets


So I am working on a small hobby project using Phaser 3 and have run into some issues working with Tile map layers.

Right now I am creating a tilemap layer and using multiple spritesheets I'm loading in. I used to have several layers I used to handle this and that worked great, however I was looking to consolidate this down to one layer since it appears that there is functionality for using multiple tilesets in one layer (see the tileset options for an array of tilesets). I've run into the issue that after setting it up and trying to place tiles from the specified tileset It is only ever pulling the sprites from the first one at the index of whatever tileset I'm trying to pull from. so if I dump all the tiles in a grid I get this:enter image description here

You see the first tilesheet is being repeated even though the lengths of each are correct. So I get all 350 tiles from the first sheet, then 318 or so for the second, then 60, then 30 but all of them start pulling the original spritesheet's images.

Here's the relevant code:

initTileSets() {
    // Initialize tilemap
    this.bgTilemap = this.scene.make.tilemap({
        width: this.mapWidth,
        height: this.mapHeight,
        tileWidth: this.tileSize,
        tileHeight: this.tileSize
    });

    // Add all tilesets to the tilemap
    this.terrainTiles = this.bgTilemap.addTilesetImage('terrainTileset');
    this.terrainTiles2 = this.bgTilemap.addTilesetImage('terrainTileset2');
    this.rogueTiles = this.bgTilemap.addTilesetImage('rogueTileset');
    this.snowTiles = this.bgTilemap.addTilesetImage('snowTileset');

    //Set the length of each tileset
    let terrainTilesetLength = this.terrainTiles.total;
    let terrainTileset2Length = this.terrainTiles2.total;
    let rogueTilesetLength = this.rogueTiles.total;
    let snowTilesetLength = this.snowTiles.total;

    //Set the first gid of each tileset to the total of the previous tileset
    this.terrainTiles.firstgid = 0;
    this.terrainTiles2.firstgid = terrainTilesetLength;
    this.rogueTiles.firstgid = terrainTilesetLength + terrainTileset2Length;
    this.snowTiles.firstgid = terrainTilesetLength + terrainTileset2Length + rogueTilesetLength;

    // Store tileset information
    this.tilesetsInfo = [
        { name: 'terrainTileset', firstgid: this.terrainTiles.firstgid },
        { name: 'terrainTileset2', firstgid: this.terrainTiles2.firstgid },
        { name: 'rogueTileset', firstgid: this.rogueTiles.firstgid },
        { name: 'snowTileset', firstgid: this.snowTiles.firstgid }
    ];

    let allTilesets = [
        this.terrainTiles,
        this.terrainTiles2,
        this.rogueTiles,
        this.snowTiles
    ];

    // Create two main layers
    this.terrainLayer = this.bgTilemap.createBlankLayer('terrainLayer', allTilesets, 0, 0);

    this.detailLayer = this.bgTilemap.createBlankLayer('detailLayer', allTilesets, 0, 0);

    //log detaillayer
    console.log(this.detailLayer);

    // Optionally, you can manage all layers together if you need to apply settings to all
    this.allLayers = [
        this.terrainLayer,
        this.detailLayer
    ];
}

getTilesetForIndex(globalIndex) 
{
    for (let i = this.tilesetsInfo.length - 1; i >= 0; i--) 
    {
        let tileset = this.tilesetsInfo[i];
        if (globalIndex >= tileset.firstgid) 
        {
            return this.bgTilemap.getTileset(tileset.name);
        }
    }
    return null;
}

placeTile(x, y, globalIndex, layer) 
{
    let tileset = this.getTilesetForIndex(globalIndex);
    if (!tileset) 
    {
        console.error(`Tileset not found for global index: ${globalIndex}`);
        return;
    }

    //log name of tileset, first gid, and global index
    //console.log('tileset: ' + tileset.name + ' | firstgid: ' + tileset.firstgid + ' | globalIndex: ' + globalIndex);
    // Calculate the local tile index within the selected tileset
    let localIndex = globalIndex - tileset.firstgid;

    // Place the tile
    layer.putTileAt(localIndex, x, y, false, tileset.name);
}

getTotalTilesInLayer(layer) 
{
    let totalTiles = 0;
    layer.tileset.forEach(tileset => {
        totalTiles += tileset.total;
    });
    return totalTiles;
}

placeDebugTileLine()
{
    for (let i = 0; i < this.getTotalTilesInLayer(this.detailLayer); i++) 
    {

        let row = Math.floor(i/25);
        let col = i % 25;
        
        this.placeTile(col, row, i, this.detailLayer);
    }
}

One thing I have noticed is that in placeTile() the layer.putTileAt method seems to ignore the final parameter. I can put in 'invalidTileSetNameHere' and it will continue as normal in the screenshot which seems odd given the documentation on it.

Update: Using info from the response I now have this. However I get an undefined error once the map.putTileAt loop reaches i = 350 which is the first index after the first tileMap's length (terrainTilemap) has been exhausted. errorlog

initTileSets2()
{
    let map = this.scene.make.tilemap({
        key:'map',
        width: 16,
        height: 16,
        tileWidth: 16,
        tileHeight: 16
    });

    // Add all tilesets to the tilemap
    let tiles1 = map.addTilesetImage('terrainTileset'); // 350 tiles
    let tiles2 = map.addTilesetImage('terrainTileset2'); // 324 tiles
    let tiles3 = map.addTilesetImage('rogueTileset'); // 60 tiles
    let tiles4 = map.addTilesetImage('snowTileset'); // 30 tiles
    
    //Set the first gid of each tileset to the total of the previous tileset
    tiles1.firstgid = 0;
    tiles2.firstgid = tiles1.total;
    tiles3.firstgid = tiles2.firstgid + tiles2.total;
    tiles4.firstgid = tiles3.firstgid + tiles3.total;

    let allTilesets = [
        tiles1,
        tiles2,
        tiles3,
        tiles4
    ];

    let layer1 = map.createBlankLayer('layer1', allTilesets, 20, 50).setScale(0.75);
        
    let tilePerRow = 25;
    for (let i = 0; i < tiles4.firstgid; i++) {
        let col = i % tilePerRow;
        let row  = Math.floor( i / tilePerRow );

        console.log('i: ' + i + ' | col: ' + col + ' | row: ' + row);
        map.putTileAt( i, col, row )
    }

    this.map = map;
}

One other oddity I've found: if I hardcode to stop at i = 350 (length of the first tileset) and remove the updates to the firstgid for each set of tiles, I get the following render. enter image description here

This appears to be each tileset being rendered on top of the previous one starting and index 0. So the tiles are all in the map, just when I try to make them not get stacked on top of eachother/replace the previous tileset's tiles I get the scenario before.


Solution

  • The last thing you commented about layer.putTileAt(), works as designed. You are calling the function on the layer, not on the map. The layer doesn't have a fifth parameter (link to documentation), and on the map function , it I would be the layer.

    SideQuestion: Was there an error in the console?

    But As far as I can see it it works, I just encountered a minor bug, I'm not sure if of my code or phaser, I added a comment in my code, how I "fixed it".

    Small Demo, that does more ore less the same as your code:
    (There seems to be a minor bug in phaser , or my code that's what the last created texture had to be bigger)

    Maybe you can use this demo to help find your error.

    document.body.style = 'margin:0;';
    
    var config = {
        width: 536,
        height: 183,
        scene: { create },
    }; 
    
    new Phaser.Game(config);
    console.clear();
    
    function create () {
        this.add.text(10,10, 'OUTPUT')
            .setScale(1.5)
            .setOrigin(0)
            .setStyle({fontStyle: 'bold', fontFamily: 'Arial'});
    
        // Creating Textures for the tilesets
        let graphics  = this.make.graphics();
        graphics.fillStyle(0xffffff);
        graphics.fillRect(0, 0, 10, 10);
        graphics.fillStyle(0x0);
        graphics.fillRect(10, 0, 10, 10);
        graphics.generateTexture('tileset_1', 20, 10);
        
        graphics.fillStyle(0xff0000);
        graphics.fillRect(0, 0, 10, 10);
        graphics.fillStyle(0xcdcdcd);
        graphics.fillRect(10, 0, 10, 10);
        graphics.generateTexture('tileset_2', 20, 10);
        
        graphics.fillStyle(0x00ff00);
        graphics.fillRect(0, 0, 10, 10);
        graphics.fillStyle(0xffff00);
        graphics.fillRect(10, 0, 10, 10);
        graphics.generateTexture('tileset_3', 20, 10);
        
        graphics.fillStyle(0x0000ff);
        graphics.fillRect(0, 0, 10, 10);
        graphics.fillStyle(0xff00ff);
        graphics.fillRect(10, 0, 10, 10);
        
        // BUG (maybe): BugFixing had to make the image bigger
        graphics.generateTexture('tileset_4', 90, 10);
        
        // END -> Creating Textures for the tilesets
        initTileSets.call(this);
    
    }
    
    function initTileSets() {
     
       let map = this.make.tilemap({
            key:'map',
            width: 10,
            height: 10,
            tileWidth: 10,
            tileHeight: 10
        });
    
        // Add all tilesets to the tilemap
        let tiles1 = map.addTilesetImage('tileset_1');
        let tiles2 = map.addTilesetImage('tileset_2');
        let tiles3 = map.addTilesetImage('tileset_3');
        let tiles4 = map.addTilesetImage('tileset_4');
    
        //Set the length of each tileset
        let tile1Length = tiles1.total;
        let tile2Length = tiles2.total;
        let tile3Length = tiles3.total;
        let tile4Length = tiles4.total;
        
        //Set the first gid of each tileset to the total of the previous tileset
        tiles1.firstgid = 0;
        tiles2.firstgid = tile1Length;
        tiles3.firstgid = tiles2.firstgid + 2;
        tiles4.firstgid = tiles3.firstgid + 2;
        
        // Store tileset information
        this.tilesetsInfo = [
            { name: 'tiles1', firstgid: tiles1.firstgid },
            { name: 'tiles2', firstgid: tiles2.firstgid },
            { name: 'tiles3', firstgid: tiles3.firstgid },
            { name: 'tiles4', firstgid: tiles4.firstgid }
        ];
    
        let allTilesets = [
            tiles1,
            tiles2,
            tiles3,
            tiles4
        ];
    
        let layer1 = map.createBlankLayer('layer1', allTilesets, 20, 50)
            .setScale(2);
            
        let tilePerRow = 3;
        for (let i = 0; i < 8; i++) {
            let col = i % tilePerRow;
            let row  = Math.floor( i / tilePerRow );
            map.putTileAt( i, col, row )
        }
    
        this.map = map;
    }
    <script src="//cdn.jsdelivr.net/npm/phaser/dist/phaser.min.js"></script>

    Update2:

    I posted this issue in the phaser discourse forum, showed they an option, how to solve this issue in a different way. (also a great place to find specific phaser help)
    Simply adding the gid, in the "creation" of the tileset image function addTilesetImage, instead of afterwards.

    Simple remove this lines

    tiles1.firstgid = 0;
    tiles2.firstgid = tile1Length;
    tiles3.firstgid = tiles2.firstgid + 2;
    tiles4.firstgid = tiles3.firstgid + 2;
    

    And alter addTilesetImage lines, to add the gid as 7th parameter (link to documentation).

    // Add all tilesets to the tilemap
    let tiles1 = map.addTilesetImage('tileset_1','tileset_1', 
        10,10,
        0,0,
        0); // <- GID
    let tiles2 = map.addTilesetImage('tileset_2','tileset_2', 
        10,10,
        0,0,
        2); // <- GID
    let tiles3 = map.addTilesetImage('tileset_3','tileset_3',
        10,10,
        0,0,
        4); // <- GID
    let tiles4 = map.addTilesetImage('tileset_4', 'tileset_4',
        10,10,
        0,0,
        6); // <- GID
    

    And than you wouldn't need the aforementioned workaround.