How to draw tiles as shapes (rectangles) dynamically

Hello there.

I’ve been wanting to develop a feature, where a user can draw tiles in the shape of rectangles, by using the this.input.activePointer.isDown method (or justDown). This phaser 3 example does something similar…

https://phaser.io/examples/v3/view/game-objects/tilemap/dynamic/get-tiles-in-shape

…but I want to see the tiles change based on its range of the rectangle, like something you’d see in god games, ala Theme Hospital or Sim City.

I can make a square shape, but it does not retain the previous tiles, which I would like it to do, preferably with a mouse press. By the second mouse press, a square is drawn and has gone back to the previous state, ready to be used again. The logic behind implementing such a feature eludes me, and am spending a lot of time wrangling with code, but progressing very little. Apologies if my description sounds difficult to understand.

I’ve managed to get the tiles to draw starting startX and startY at 0, which is not being controlled by the user yet, while the endX and endY follow the pointer.

The example above clones each point clicked, p1 and p2 with its own x and y cooardinates. I tried to use that code to my advantage, but have found the logic difficult to understand.

Ideally, it’ll be like drawing rectangles out of tiles, changing the tiles that are in the range of the rect, accomplished using the getTilesWithinShape function which takes in a rect.

https://github.com/WinstonItiDev/tile-create-example

let p1 = null;
let p2 = null;
let map = null;

let overlappingTiles = [];
let rect = null
let worldPoint = null

export class GameScene extends Phaser.Scene {
    constructor() {
        super('Game')
    }

    create() {
        map = this.make.tilemap({ key: 'map' })
        let tiles = map.addTilesetImage('cybernoid')
        let layer = map.createDynamicLayer(0, tiles);
        layer.setScale(2, 2);

        map.setCollisionByExclusion(7);

        this.cameras.main.setBounds(0, 0, map.widthInPixels, map.heightInPixels)
    }

    update(time, delta) {
        worldPoint = this.input.activePointer.positionToCamera(this.cameras.main)

        overlappingTiles = [];

        rect = new Phaser.Geom.Rectangle(0, 0, worldPoint.x, worldPoint.y)
        overlappingTiles = map.getTilesWithinShape(rect)

        overlappingTiles.forEach((tile) => {
            if (tile) {
                tile.index = 1
            }
        })

        // if (this.input.activePointer.justDown) {
        //     p1 = worldPoint.clone()
        //     console.log(p1);
        //     // xStart = Math.min(0, worldPoint.x);
        //     // yStart = Math.min(0, worldPoint.y);
        //     // xEnd = Math.max(worldPoint.x, worldPoint.x);
        //     // yEnd = Math.max(worldPoint.y, worldPoint.y);

        // if (!p1) {
        //     p1 = worldPoint.clone()
        // } else if (!p2) {
        //     p2 = worldPoint.clone()
        // } else {
        //     p1 = worldPoint.clone()
        //     p2 = null
        // }
    }
}

Heres my current code. I’ll be updating it each time I make an improvement.

If anyone can offer me some advice on how to approach this problem, or any help with understand the code, it would be much appreciated. Feel free to Pm me. Thanks.

I actually made improvements. Heres my code so far:

let p1 = null;
let p2 = null;
let map = null;
let overlappingTiles = [];
let rect = null
let worldPoint = null
let layer = null
let groundLayer = null

export class GameScene extends Phaser.Scene {
    constructor() {
        super('Game')
    }

    create() {
        map = this.make.tilemap({ key: 'map' })
        let tiles = map.addTilesetImage('cybernoid')
        layer = map.createDynamicLayer(0, tiles);
        groundLayer = map.createDynamicLayer(1, tiles)
        layer.setScale(2, 2);
        groundLayer.setScale(2, 2);

        map.setCollisionByExclusion(7);

        this.cameras.main.setBounds(0, 0, map.widthInPixels, map.heightInPixels)

        this.input.on('pointerdown', (pointer) => {
        })
    }

    update(time, delta) {

        if (this.input.activePointer.justDown) {
            let initialPoint = this.input.activePointer.positionToCamera(this.cameras.main)
            if (!p1) {
                p1 = initialPoint.clone()
            } else if (!p2) {
                // clone
            } else {
                // has data
                // null
            }
        }

        if (p1) {
            // declare the ground layer tile as 0
            // to utilize the alpha channel
            map.forEachTile(function (tile) {
                tile.index = 0
            });

            worldPoint = this.input.activePointer.positionToCamera(this.cameras.main)

            // just like in the phaser 3 get tiles in shape example
            overlappingTiles = [];

            let xStart = Math.min(p1.x - 5, worldPoint.x);
            let yStart = Math.min(p1.y - 5, worldPoint.y);
            let xEnd = Math.max(p1.x - 5, worldPoint.x);
            let yEnd = Math.max(p1.y - 5, worldPoint.y);

            rect = new Phaser.Geom.Rectangle(xStart, yStart, xEnd - xStart, yEnd - yStart);

            overlappingTiles = map.getTilesWithinShape(rect, groundLayer)

            // set tile index 
            overlappingTiles.forEach((tile) => {
                tile.index = 7
            })
        }

        // map.removeTileAtWorldXY(worldPoint.x, worldPoint.y)
        // if (this.input.activePointer.justDown) {
        //     p1 = worldPoint.clone()
        //     console.log(p1);
            // xStart = Math.min(0, worldPoint.x);
            // yStart = Math.min(0, worldPoint.y);
            // xEnd = Math.max(worldPoint.x, worldPoint.x);
            // yEnd = Math.max(worldPoint.y, worldPoint.y);

        // if (!p1) {
        //     p1 = worldPoint.clone()
        // } else if (!p2) {
        //     p2 = worldPoint.clone()
        // } else {
        //     p1 = worldPoint.clone()
        //     p2 = null
        // }
    }
}

Now if you click once on the screen, you should see a tile, and it should expand whichever way the mouse leads. Yusss! Phew! I’ve made some progress. Now I just need to have a second press, which will establish the tiles in place. Maybe the layer could be converted to a static layer after the second mouse press, I’m sure that’s possible?

Here is my github repo: https://github.com/WinstonItiDev/tile-create-example

I’ll keep pushing to github each time I make an adjustment, and update this post as I go.

EDIT:

let p1 = null;
let p2 = null;
let map = null;
let overlappingTiles = [];
let rect = null
let dragPoint = null
let groundLayer = null
let layer = null
let newStaticLayer = null

    update(time, delta) {
        if (this.input.activePointer.justDown) {
            let firstPoint = this.input.activePointer.positionToCamera(this.cameras.main)
            if (!p1) {
                p1 = firstPoint.clone()
            } 
            else if (!p2) {
                p2 = firstPoint.clone()
            }
            console.log(p1, p2);
        }

        if (p1) {
            // declare the ground layer tile as 0
            // to utilize the alpha channel

            map.forEachTile(function (tile) {
                tile.index = 0
            });

            dragPoint = this.input.activePointer.positionToCamera(this.cameras.main)

            // just like in the phaser 3 get tiles in shape example

            overlappingTiles = [];

            let xStart = Math.min(p1.x - 5, dragPoint.x);
            let yStart = Math.min(p1.y - 5, dragPoint.y);
            let xEnd = Math.max(p1.x - 5, dragPoint.x);
            let yEnd = Math.max(p1.y - 5, dragPoint.y);

            rect = new Phaser.Geom.Rectangle(xStart, yStart, xEnd - xStart, yEnd - yStart);

            overlappingTiles = map.getTilesWithinShape(rect, groundLayer)

            // set tile index to draw
            overlappingTiles.forEach((tile) => {
                tile.index = 7
            })
        }

        if (p2) {
            newStaticLayer = map.convertLayerToStatic(groundLayer)
        }
    }
}

If p2 (the second mouse press) then convert groundLayer to a static layer, so one can’t manipulate it. This seems to work, although the static layer scales to 0, I want it to stay the same scale as it was when it was a dynamic layer.

I console logged the newStaticLayer variable, expect to see an static layer object. Since it is in the update function, I would expect the object to show in the console infinitely. It only shows once, and defaults to null infinitely.

I can’t access the functions as a static layer, such as scale or alpha. Is it returning null due to being wrapped around a conditional?

If anyone could help me understand why this is happening and what possible solutions could be done, I will really appreciate!! :slight_smile: :innocent:

p2 remains true, so it keeps converting a layer that was deleted after the first conversion…

1 Like

Hey that really helped!

Thanks for your help. Now I’m trying to do it without converting it to a static layer… retain the dynamicTileLayer. It seems I can only create a square once, because I’m creating a layer and then destroying it by converting the dynamic layer into a static layer, becoming unusable, making it unable to make a new square of triangles.

How can I continuously add to the tiles (overlappingTiles = []) array when I decide to draw a square of tiles? I want to store the tiles after the second click, and then continue drawing squares, adding to the tile array. I want to be able to do this without deleting the original dynamic map. I haven’t been able to figure this one out just yet.

Here is some of my recent code:

let p1 = null;
let p2 = null;
let map = null;
let overlappingTiles = [];
let rect = null
let firstPoint = null
let dragPoint = null
let groundLayer = null
let layer = null
let newStaticLayer = null

export class GameScene extends Phaser.Scene {
    constructor() {
        super('Game')
    }

    create() {
        map = this.make.tilemap({ key: 'map' })
        let tiles = map.addTilesetImage('cybernoid')
        layer = map.createDynamicLayer(0, tiles);
        groundLayer = map.createDynamicLayer(1, tiles)
        layer.setScale(2, 2);
        groundLayer.setScale(2, 2);

        map.setCollisionByExclusion(7);

        this.cameras.main.setBounds(0, 0, map.widthInPixels, map.heightInPixels)
    }

    update(time, delta) {

        let pointer = this.input.activePointer
        if (pointer.justDown) {
            firstPoint = this.input.activePointer.positionToCamera(this.cameras.main)
            if (!p1) {
                p1 = firstPoint.clone()
            }
            else if (!p2) {
                p2 = firstPoint.clone()

            }
            console.log(p1, p2);
        }

        if (p1) {
            // declare the ground layer tile as 0
            // to utilize the alpha channel
            map.forEachTile(function (tile) {
                tile.index = 0
            });

            dragPoint = this.input.activePointer.positionToCamera(this.cameras.main)

            // just like in the phaser 3 get tiles in shape example
            overlappingTiles = [];

            let xStart = Math.min(p1.x - 5, dragPoint.x);
            let yStart = Math.min(p1.y - 5, dragPoint.y);
            let xEnd = Math.max(p1.x - 5, dragPoint.x);
            let yEnd = Math.max(p1.y - 5, dragPoint.y);

            rect = new Phaser.Geom.Rectangle(xStart, yStart, xEnd - xStart, yEnd - yStart);

            overlappingTiles = map.getTilesWithinShape(rect, groundLayer)

            // set tile index to draw
            overlappingTiles.forEach((tile) => {
                tile.index = 7
            })
        }
        if (p2) {
            // once triggered, convert dynamic layer to static layer
            newStaticLayer = map.convertLayerToStatic(groundLayer)
            // set scale to fit original dynamic layer
            newStaticLayer.setScale(2, 2)
            newStaticLayer.alpha = 0.2
            // reads new static layer object
            console.log(newStaticLayer);
            // disable this conditional, to stop from looping
            p2 = false
            p1 = false
            pointer.enabled = false
        }
    }
}

I’m not sure.
Maybe you could just recreate groundLayer again when p2 is clicked?

I am a bit conflicted here because you’re using an obsolete phaser version (3.15?). Rich has mentioned Static Layers are gone, and so is justDown…

1 Like

I heard what you were saying, so I decided to update it. Now it just uses the this.input.on listener from the create function, while it updates in the update function. Heres the code:

let p1 = null;
let p2 = null;
let map = null;
let overlappingTiles = [];
let rect = null
let firstPoint = null
let dragPoint = null
let groundLayer = null
let layer = null

export class GameScene extends Phaser.Scene {
    constructor() {
        super('Game')
    }

    create() {
        map = this.make.tilemap({ key: 'map' })
        let tiles = map.addTilesetImage('cybernoid')
        groundLayer = map.createDynamicLayer(0, tiles)
        groundLayer.setScale(2, 2);

        layer = map.createDynamicLayer(1, tiles)
        layer.setScale(2, 2)

        this.cameras.main.setBounds(0, 0, map.widthInPixels, map.heightInPixels)

        this.input.on('pointerdown', () => {
            firstPoint = this.input.activePointer.positionToCamera(this.cameras.main)
            if (!p1) {
                p1 = firstPoint.clone()
                console.log(p1, p2);
            }
            else if (!p2) {
                p2 = firstPoint.clone()
                console.log(p1, p2);
            }
        })
    }

    update(time, delta) {
        if (p1) {
            // declare the ground layer tile as 0
            // to utilize the alpha channel
            map.forEachTile(function (tile) {
                tile.index = 0
            });

            dragPoint = this.input.activePointer.positionToCamera(this.cameras.main)

            overlappingTiles = [];

            let xStart = Math.min(p1.x - 5, dragPoint.x);
            let yStart = Math.min(p1.y - 5, dragPoint.y);
            let xEnd = Math.max(p1.x - 5, dragPoint.x);
            let yEnd = Math.max(p1.y - 5, dragPoint.y);

            rect = new Phaser.Geom.Rectangle(xStart, yStart, xEnd - xStart, yEnd - yStart);

            overlappingTiles = map.getTilesWithinShape(rect, layer)

            // set tile index to draw
            overlappingTiles.forEach((tile) => {
                tile.index = 7
            })

            // console.log(rect.height);
            // if (rect.width > 200) {
            // let result = overlappingTiles.filter((result) => {
            //     result > 5
            // })
            // }
        }
        if (p2) {
            p2 = false
            p1 = false
        }
    }
}

I have a few ideas of where I want to go with this, like instead of restart the square of tiles everytime I click, have it draw tiles and still have the previous ones in place, like if you were painting.

Also, I noticed if I have two layers, one ground layer and upper layer, I can set the tile index to 0, which is transparent, to every tile outside of the square. That’s why I went along with manipulating an upper layer. Would I be correct to say I could just use the tiles in the original map, set the index forEachTile? to transparent, and then draw on top of it? That would also mean I could use two maps, one for the ground, and one to draw on.

Anyways, thanks for the help.