Json map saving in phaser 3 is not working

71 Views Asked by At

I am making my own world editor from a folder of individual tiles that you can place on the so called map. The tile placing works but when I try to save the map the json file map data indicates that the map is empty.

The map and layer are created as blank so you can create your own but after placing tiles they are still undefined and blank and I dont know how to fix the problem

There is a fake tileset i am using for my layer because otherwise the tile placing doesnt work

this is my code

class WorldEditor extends Phaser.Scene {
    constructor() {
        super("editWorld")
        this.tileSize = 48
        this.tileIndex = 0
        this.tileFolder = 'Assets/Tiles/'
        this.tilesPerRow = 37
        this.totalTiles = 160
        this.tilePicked = false
        this.tileset = []
        this.dragging = false
    }

    preload() {
        // Load tile images from the folder
        for (let i = 0; i < this.totalTiles; i++) {
            this.load.image('tile (' + i + ')', this.tileFolder + 'tile (' + i + ').png')
        }
    }

    create() {
        const tileWidth = this.tileSize
        const tileHeight = this.tileSize
        const tilesPerRow = this.tilesPerRow
    
        if(!this.map){
            this.map = this.make.tilemap({ 
                tileWidth: this.tileSize, 
                tileHeight: this.tileSize,
                width: 1200,
                height: 720
            })  

            const tileset = this.map.addTilesetImage('tiles', 'tiles', this.tileSize, this.tileSize)
            this.layer = this.map.createBlankLayer('layer', tileset)
        }

        this.input.keyboard.on('keydown-S', () => {
            console.log('map saved')
            this.saveMapData(this.map)
        })

        for (let i = 0; i < this.totalTiles; i++) {
            const x = ((i % tilesPerRow) * tileWidth)/1.48
            const y = (Math.floor(i / tilesPerRow) * tileHeight)/1.48
    
            this.tile = this.add.image(x, y, 'tile (' + i + ')')
            this.tile.setOrigin(0)
            this.tile.setScale((tileWidth / this.tile.width)/1.48, (tileHeight / this.tile.height)/1.48)
            this.tileset.push(this.tile)
        }
    
        // Set up pointer events
        this.setupPointerEvents()
               
    }
    
    setupPointerEvents() {
        this.input.off('pointerdown') 
    
            const tiles = this.children.list.filter(child => child.texture && child.texture.key.startsWith('tile ('))
            tiles.forEach((tile, i) => {
                tile.setInteractive()
                tile.on('pointerdown', function() {
                    this.tileset.forEach(tile => tile.clearTint())
                    this.tileIndex = i
                    this.tilePicked = true
                    tile.setTint(0xff0000)
                }, this)
            })
            
            this.input.on('pointerdown', function(){
                this.dragging = true
            }, this)    
               
            this.input.on('pointerup', function(){
                this.dragging = false
            }, this)  

            this.input.on('pointermove', function(pointer) {
                if(this.dragging){
                this.tileX = Math.floor(pointer.x / this.tileSize)
                this.tileY = Math.floor(pointer.y / this.tileSize)
                this.placeTile(this.tileX, this.tileY)
                }
            }, this)
             
            
        
    }
    // Other methods...


    placeTile(tileX, tileY) {
        if (this.map && this.layer) {
            if (tileY < 4) {
                console.log('Cannot place tile on the tile selection area');
                return;
            }
    
            if (!this.tilePicked) {
                console.log('No tile is chosen');
            } else {
                this.map.putTileAt(this.tileIndex + 1, tileX, tileY, this.layer);
            }
        } else {
            console.log('Tilemap or layer is not properly initialized');
        }
    }

    saveMapData() {
        const mapData = {
            width: this.map.width,
            height: this.map.height,
            layers: []
        };
    
        // Iterate over each layer in the map
        this.map.layers.forEach(layer => {
            const layerData = {
                name: layer.name || 'layer', // Default name if not provided
                data: []
            };
    
            // Iterate over each row of tiles in the layer
            for (let y = 0; y < this.map.height; y++) {
                // Iterate over each tile in the row
                for (let x = 0; x < this.map.width; x++) {
                    // Get the tile index at the current position
                    const tileIndex = layer.data[y][x].index;
                    // Record the tile index
                    layerData.data.push(tileIndex);
                }
            }
    
            // Push the layer data to the map data
            mapData.layers.push(layerData);
        });
    
        const jsonString = JSON.stringify(mapData, null, 2);
    
        const blob = new Blob([jsonString], { type: 'application/json' });
    
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = 'map.json';
        a.click();
        URL.revokeObjectURL(url);
    }
    // Implement saveMap and loadMap functions to save and load level data
}

I've tried to get the data straight from the map and deleting the tileset used for the layer but if there is no tileset it doesnt work

1

There are 1 best solutions below

8
winner_joiner On

I don't see the issue, it works for me (except to auto open with click). But just to be on the save side, if everything is setup correct, you should atleast see all vaules set to -1, if this is not the case something is not configured correct.

Also important is that the tileset has atleast as many images as images that you want to place on the map, if not an error will occur.

btw.: why are you using tilemap, if you are not using tilesets, not using layers and are creating your own datastructure for the export? Would it not be easier just to use a 2D-Array (or your final export datastructure), instead of "misusing" the phaser feature, with tons of workarounds?

Update:
It depends on your usecase, but lets says:

  1. you just want to create a map with one layer
  2. need to make a download (does it real need to be exported?)
  3. need to use in in phaser as map again

Short Demo, how I would do this (using tilemap):

document.body.style = 'margin:0;';
console.clear();

class MainScene extends Phaser.Scene{
    create () {
        this.label = this.add.text(10, 10, 'No Tile selected');
         this.add.text(10, 30, 'Press "S" to create a Save-file');

        // just for the demo -- START
        let graphics  = this.make.graphics();
        graphics.fillStyle(0xffffff);
        graphics.fillRect(0, 0, 10, 10);
        graphics.fillStyle(0xfff0000);
        graphics.fillRect(10, 0, 10, 10);
        graphics.generateTexture('tileset', 20, 10);
        graphics.generateTexture('tile_0', 10, 10);
        graphics.fillStyle(0xfff0000);
        graphics.fillRect(0, 0, 10, 10);
        graphics.generateTexture('tile_1', 10, 10);
        // just for the demo -- END        
        
        this.map = this.make.tilemap({ 
            tileWidth: 10, 
            tileHeight: 10,
            width: config.width,
            height: config.height
        });
        
        let y = 50;
        for(let idx = 0; idx < 2; idx++){
            let drawTile = this.add.image(10, y, `tile_${idx}`)
                .setInteractive()
                .on('pointerdown', () => {
                        this.selectedTile = idx;
                      this.label.setText(`tile ${idx} is selected`)
                })
                .setScale(2)
                .setOrigin(0);
            y += 20;
        }

        const tileset = this.map.addTilesetImage('tileset', null, 10, 10);
        this.layer = this.map.createBlankLayer('layer1', tileset, 200, 50, 10, 10)
            .setInteractive();
        
        // MAP Border
        this.add.rectangle(200, 50, 100, 100)
            .setStrokeStyle(2, 0xffffff)
            .setOrigin(0);
        
        this.layer.on('pointerdown', (pointer) => {
            if(this.selectedTile == undefined){
                this.label.setText('First select a tile to add to the map!!');
                return;
            }
            let {x,y} = pointer;
            this.map.putTileAtWorldXY(this.selectedTile, x, y);
        });
        
        this.input.keyboard.on('keydown-S', () => {
        
       
       
       const mapData = {
            width: this.map.width,
            height: this.map.height,
            layers: []
        };
    
        // Iterate over each layer in the map
        this.map.layers.forEach(layer => {
            const layerData = {
                name: layer.name || 'layer', // Default name if not provided
                data: layer.data
            };
    
            // Push the layer data to the map data
            mapData.layers.push(layerData);
        });
        
        
        console.info(this.map, mapData)
       
       
            let data = this.layer.layer.data.map( row => row.map(col => col.index));
            let a = document.createElement('a');
            let br = document.createElement('br');
            let jsonString = JSON.stringify(data);
            let blob = new Blob([jsonString], { type: 'application/json' })
            a.href = window.URL.createObjectURL(blob);
            a.download = 'test.json';
            a.innerText = 'Download';
            a.target = '_blank';

            document.body.append(br,a);
        })
    }
}

var config = {
    width: 536,
    height: 183,
    scene: MainScene
}; 

new Phaser.Game(config);
<script src="//cdn.jsdelivr.net/npm/phaser/dist/phaser.min.js"></script>

Important is, that the tileset has enough tiles, and you limit the the layers to the exact col & row size.

And for "importing" the data later I would use a 2D array to create the map, like in this example.