Get the full commented source code of

HTML5 Suika Watermelon Game

Talking about Hexagonal Tiles game, Game development, HTML5, Javascript and Phaser.

One of the most visited posts of the blog is How to find adjacent tiles in hexagonal maps – ALL and EVERY case explained.

There seems to be a lot of interest around hexagon maps, and although in the original post I covered all and every possible case, I used two different scripts.

So it’s time to build a single script to manage all hexagon maps, updated to latest Phaser version, with some further improvements.

Why am I talking about different cases? Because there are four kinds of hexagon maps, look at this picture:

These are the four different hexagon maps we may have to deal with, let’s see them more in detail:

UPPER LEFT: This is a vertical odd hex map, so each column is basically made by two hexagon columns. The upper-left hexagon is at coordinate (0,0), then the hexagon placed exactly below is at coordinate (0,2). To find the hexagon at coordinate (0,1) we must look at the exagon touching the bottom-right side.

As you can see in the picture, having an odd number of columns – nine – will also cause our map to have some missing coordinates in the 4th column, such as (4,1), (4,3), (4,5) and so on with all odd y numbers until (4,11). This may cause errors when looking at adjacent tiles, because not all columns are complete.

Also, finding adjacent tiles follows different rules according to y coordinate, which may be even or odd. Just look at hexagon (2,4): to find its lower left adjacent tile we must look at (x – 1, y + 1), while to find the lower left adjacent tile of (2, 5) we must look at (x, y + 1).

This is how we manage this kind of hex map:

Move the mouse around the map to highlight selected hexagon and adjacent hexagons.

UPPER RIGHT:

A vertical even hex map. It’s the same thing as upper left example, but this is easier to work with as it features an even number of columns, and you don’t have to deal with missing coordinates.

This is how we manage this kind of hex map:

Move the mouse around the map to highlight selected hexagon and adjacent hexagons.

LOWER LEFT:

Horizontal hex maps basically feature the same problems found in vertical hex maps. In this case, a horizontal even hex map, an even number of rows – remember, “rows” as we are talking about horizontal maps now – prevents us to deal with missing coordinates, but there are different rules to determine adiacent tiles according to hexagon y coordinate being even or odd: if we refer at (1, 7) as example, the bottom left hexagon will be at (x – 1, y – 1), but the bottom left hexagon of (1, 8) is at (x, y – 1).

This is how we manage this kind of hex map:

Move the mouse around the map to highlight selected hexagon and adjacent hexagons.

LOWER RIGHT:

The lower right example is a horizontal odd hex map, a map with an odd number of lines, so you will have to deal with missing row tiles. You don’t have (3, 1), (3, 3) and so on.

This is how we manage this kind of hex map:

Move the mouse around the map to highlight selected hexagon and adjacent hexagons.

The two scripts in the original post have been merged in a single script, which is more customizable thanks to circumRadius global variable which allows to set the radius of the circumference where the hexagon is inscribed.

Have a look, all above examples have been made just playing with gameOptions global object:

let game;

const HORIZONTAL = 0;
const VERTICAL = 1;

let gameOptions = {
    circumRadius: 40,
    gridSizeX: 17,
    gridSizeY: 8,
    gridType: HORIZONTAL
}

window.onload = function() {
    let gameConfig = {
        type: Phaser.AUTO,
        backgroundColor: 0xffffff,
        scale: {
            mode: Phaser.Scale.FIT,
            autoCenter: Phaser.Scale.CENTER_BOTH,
            parent: "thegame",
            width: 624,
            height: 480
        },
        scene: playGame
    }
    game = new Phaser.Game(gameConfig);
    window.focus();
}

class playGame extends Phaser.Scene {
    constructor() {
        super("PlayGame");
    }
    preload() {
        this.load.image("verticalhexagon", "verticalhexagon.png");
        this.load.image("horizontalhexagon", "horizontalhexagon.png");
    }
    create() {
        const pi6 = Math.cos(Math.PI / 6);
        this.hexagonWidth = 2 * gameOptions.circumRadius * ((gameOptions.gridType == VERTICAL) ? 1 : pi6);
        this.hexagonHeight =  2 * gameOptions.circumRadius * ((gameOptions.gridType == VERTICAL) ? pi6 : 1);
        this.sectorWidth = (gameOptions.gridType == VERTICAL) ? this.hexagonWidth / 4 * 3 : this.hexagonWidth;
        this.sectorHeight = (gameOptions.gridType == VERTICAL) ? this.hexagonHeight : this.hexagonHeight / 4 * 3;
        this.gradient = (gameOptions.gridType == VERTICAL) ? (this.hexagonWidth / 4) / (this.hexagonHeight / 2) : (this.hexagonHeight / 4) / (this.hexagonWidth / 2);
        let columNumber = (gameOptions.gridType == VERTICAL) ? gameOptions.gridSizeY / 2 : gameOptions.gridSizeX / 2
        this.columns = [Math.ceil(columNumber), Math.floor(columNumber)];
        this.hexagonArray = [];
        for(let i = 0; i < ((gameOptions.gridType == VERTICAL) ? gameOptions.gridSizeX : gameOptions.gridSizeY) / 2; i ++) {
            this.hexagonArray[i] = [];
            for(let j = 0; j < ((gameOptions.gridType == VERTICAL) ? gameOptions.gridSizeY : gameOptions.gridSizeX); j ++) {
                let controller = (gameOptions.gridType == VERTICAL) ? gameOptions.gridSizeX : gameOptions.gridSizeY;
                if(controller % 2 == 0 || i + 1 < controller / 2 || j % 2 == 0) {
                    let hexagonX = (gameOptions.gridType == VERTICAL) ? (this.hexagonWidth * i * 1.5 + (this.hexagonWidth / 4 * 3) * (j % 2)) : (this.hexagonWidth * j / 2);
                    let hexagonY = (gameOptions.gridType == VERTICAL) ? (this.hexagonHeight * j / 2) : (this.hexagonHeight * i * 1.5 + (this.hexagonHeight / 4 * 3) * (j % 2));
                    let hexagon = this.add.sprite(hexagonX, hexagonY, gameOptions.gridType == VERTICAL ? "verticalhexagon" : "horizontalhexagon");
                    hexagon.setOrigin(0, 0);
                    this.hexagonArray[i][j] = hexagon;
                    this.add.text(hexagonX + this.hexagonWidth / 2, hexagonY + this.hexagonHeight / 2, i + "," + j, {
                        color: "#000000"
                    }).setOrigin(0.5, 0.5);
                }
            }
        }
        this.input.on("pointermove", this.checkMove, this);
    }
    checkMove(e) {
        let candidateX = (gameOptions.gridType == VERTICAL) ? Math.floor(e.x / this.sectorWidth) : Math.floor(e.y / this.sectorHeight);
        let candidateY = (gameOptions.gridType == VERTICAL) ? Math.floor(e.y / this.sectorHeight) : Math.floor(e.x / this.sectorWidth);
        let deltaX = (gameOptions.gridType == VERTICAL) ? (e.x % this.sectorWidth) : ( e.y % this.sectorHeight);
        let deltaY = (gameOptions.gridType == VERTICAL) ? (e.y % this.sectorHeight) : (e.x % this.sectorWidth);
        let actualWidth = (gameOptions.gridType == VERTICAL) ? this.hexagonWidth : this.hexagonHeight;
        let actualHeight = (gameOptions.gridType == VERTICAL) ? this.hexagonHeight : this.hexagonWidth;
        if(candidateX % 2 == 0) {
            if(deltaX < ((actualWidth / 4) - deltaY * this.gradient)) {
                candidateX --;
                candidateY --;
            }
            if(deltaX < ((-actualWidth / 4) + deltaY * this.gradient)) {
                candidateX --;
            }
        }
        else {
            if(deltaY >= actualHeight / 2) {
                if(deltaX < (actualWidth / 2 - deltaY * this.gradient)) {
                    candidateX --;
                }
            }
            else {
                if(deltaX < deltaY * this.gradient) {
                    candidateX --;
                }
                else {
                    candidateY --;
                }
            }
        }
        this.placeMarker((gameOptions.gridType == VERTICAL) ? candidateX : candidateY, (gameOptions.gridType == VERTICAL) ? candidateY : candidateX);
    }
    placeMarker(posX, posY) {
        let candidateX = (gameOptions.gridType == VERTICAL) ?  gameOptions.gridSizeX : gameOptions.gridSizeY;
        let candidateY = (gameOptions.gridType == VERTICAL) ?  gameOptions.gridSizeY : gameOptions.gridSizeX;
        for(let i = 0; i < candidateX / 2; i ++) {
            for(let j = 0; j < candidateY; j ++) {
                if(candidateX % 2 == 0 || (i + 1) < candidateX / 2 || j % 2 == 0) {
                    this.hexagonArray[i][j].tint = 0xffffff;
                }
            }
        }
        let actualX = (gameOptions.gridType == VERTICAL) ? posX : posY;
        let actualY = (gameOptions.gridType == VERTICAL) ? posY : posX;
        if(actualX >= 0 && actualY >= 0 && actualX < candidateX && actualY <= this.columns[actualX % 2] - 1) {
            let markerX = Math.floor(actualX / 2);
            let markerY = actualY * 2 + actualX % 2;
            this.hexagonArray[markerX][markerY].tint = 0x00ff00;
            if(markerY - 2 >= 0) {
                this.hexagonArray[markerX][markerY - 2].tint = 0xff0000;
            }
            let actualGridSizeX = (gameOptions.gridType == VERTICAL) ? gameOptions.gridSizeX : gameOptions.gridSizeY;
            let actualGridSizeY = (gameOptions.gridType == VERTICAL) ? gameOptions.gridSizeY : gameOptions.gridSizeX;
            if(markerY + 2 < actualGridSizeY) {
                this.hexagonArray[markerX][markerY + 2].tint = 0xff0000;
            }
            if(markerX + markerY % 2 < actualGridSizeX / 2 && (actualGridSizeX % 2 == 0 || markerX < Math.floor(actualGridSizeX / 2))) {
                if(markerY - 1 >= 0) {
                    this.hexagonArray[markerX + markerY % 2][markerY - 1].tint = 0xff0000;
                }
                if(markerY + 1 < actualGridSizeY) {
                    this.hexagonArray[markerX + markerY % 2][markerY + 1].tint = 0xff0000;
                }
            }
            if(markerX - 1 + markerY % 2 >= 0) {
                if(markerY - 1 >= 0) {
                    this.hexagonArray[markerX - 1 + markerY % 2][markerY - 1].tint = 0xff0000;
                }
                if(markerY + 1 < actualGridSizeY) {
                    this.hexagonArray[markerX - 1 + markerY % 2][markerY + 1].tint = 0xff0000;
                }
            }
        }
    }
}

It might be better to have a dedicated hexagon class to handle hex maps, but starting from these examples you should be able to build your hexagon tile based games. Download the source code.

Never miss an update! Subscribe, and I will bother you by email only when a new game or full source code comes out.