Friday, March 30, 2012

Maps and Boardgames (Part 1 - Introduction)


Hi all.

If you have seen some of my posts you've probably suspected that I like spatial stuff :). I would like to make an experiment where I'm going to make some kind of boardgame engine on top of a map (probably Leaflet).

It will be a little bit useless but should be a fun exercise to apply some of the stuff that I've been talking about, like html5 canvas, tile maps, leaflet, UTFGrids, etc.

So, without further ado, this is my scope:

I want to create an hexagon grid like the ones you would see in some boardgames. Some notable examples:

Settlers of Catan
Memoir 44

And then display it on top of a map and be able to interact with it. Like, while moving the mouse cursor it should automatically detect the hex I'm on. Also, be able to drag and drop some objects from hexagon to hexagon with a traveling distance limitation (like 2 hexagons in a row). Basically, get the pieces together so that I could make an ultra-simple spatial boardgame.

I honestly don't know if I'm going to do this client-side, server-side or a mix of both. I'll just go along with the flow. Also, some people told me that my posts are just too damn big, so I'll try to divide them into smaller chunks so that they're more digestible :)

In this first post I'll just stick to the hexagon theory and display a grid using the HTML5 canvas. I've taken some info from here and here. Basically, how to create a regular hexagon (all 6 sides of the same size), and the related algorithms.

I'll start by showing the end result.

This is rendered using the HTML5 canvas element. Also, it includes drag support just to move the hex grid around. You can try a working example here.


The full source-code for this page is:
<html>
<head>

    <script>

        var xOffset = 100;
        var yOffset = 50;

        var previousPointX;
        var previousPointY;

        var down = false;
        var hex;

        window.onload = function () {

            canvas = document.getElementById("myCanvas");
            
            hex = new hexDefinition(45);

            render();

            canvas.onmousedown = function (e) {
                var x;
                var y;

                down = true;

                console.log('mouse down');

                if (e.pageX || e.pageY) {
                    x = e.pageX;
                    y = e.pageY;
                }
                else {
                    x = e.clientX + document.body.scrollLeft 
                                  + document.documentElement.scrollLeft;
                    y = e.clientY + document.body.scrollTop 
                                  + document.documentElement.scrollTop;
                }
                x -= canvas.offsetLeft;
                y -= canvas.offsetTop;

                previousPointX = x;
                previousPointY = y;
            }

            canvas.onmouseup = function (e) {

                down = false;

                console.log('mouse up');
            }

            canvas.onmousemove = function (e) {

                if (down == false) {
                    return;
                } 
                
                var x;
                var y;

                if (e.pageX || e.pageY) {
                    x = e.pageX;
                    y = e.pageY;
                }
                else {
                    x = e.clientX + document.body.scrollLeft 
                                  + document.documentElement.scrollLeft;
                    y = e.clientY + document.body.scrollTop
                                  + document.documentElement.scrollTop;
                }
                x -= canvas.offsetLeft;
                y -= canvas.offsetTop;
                xOffset += (x - previousPointX);
                yOffset += (y - previousPointY);

                previousPointX = x;
                previousPointY = y;

                render();

            }
        };

        function drawHex(context, hexCoordinates) {

            var center = hex.getWorldCoordinates(hexCoordinates.u, hexCoordinates.v);

            context.moveTo((center.x - hex.b / 2.0) + xOffset, 
                           center.y + yOffset );
            context.lineTo((center.x - hex.s / 2.0) + xOffset, 
                          (center.y - hex.a / 2.0) + yOffset);
            context.lineTo((center.x + hex.s / 2.0) + xOffset, 
                          (center.y - hex.a / 2.0) + yOffset);
            context.lineTo((center.x + hex.b / 2.0) + xOffset, 
                           center.y + yOffset);
            context.lineTo((center.x + hex.s / 2.0) + xOffset, 
                          (center.y + hex.a / 2.0) + yOffset);
            context.lineTo((center.x - hex.s / 2.0) + xOffset, 
                          (center.y + hex.a / 2.0)  + yOffset);
            context.lineTo((center.x - hex.b / 2.0) + xOffset,
                           center.y + yOffset);
            context.lineWidth = 1;
            context.strokeStyle = "#444";
     context.fillStyle = "#444";
     context.textAlign = "center";
            context.textBaseline = "middle";
     context.fillText("(" + hexCoordinates.u + "," + hexCoordinates.v + ")", 
                             center.x + xOffset, center.y + yOffset );
        }

        function render() {

            var canvas = document.getElementById("myCanvas");
            var context = canvas.getContext("2d");

            context.clearRect(0, 0, 800, 600);
            drawHexagonGrid(context);
        }

        function drawHexagonGrid(context) {

            context.beginPath();
            for (var i = 0; i < 5; i++) {
                for (var j = 0; j < 5; j++) {
                    drawHex(context, new hexCoordinates(i, j));
                }
            }
            context.stroke();            
        }

        function hexDefinition(edgeSize) {

            this.s = edgeSize;

            this.h = Math.sin(30*Math.PI/180) * edgeSize;

            this.r = Math.cos(30*Math.PI/180) * edgeSize;

            this.b = edgeSize + 2 * this.h;

            this.a = 2 * this.r;

            this.hexagon_narrow_width  = this.s + this.h;
            this.hexagon_wide_width = this.b;
            this.hexagon_height = this.a;

            /*
             u - horizontal index of hex
             v - vertical index of hex
             */
            this.getWorldCoordinates = function(u, v) {

                var x = this.hexagon_narrow_width * u;
                var y = this.hexagon_height * (u*0.5 + v);

                return new worldCoordinates(x,y);
            };

            this.getHexagonalCoordinates = function(x, y) {

                var u = x / this.hexagon_narrow_width;
                var v = y / this.hexagon_height - u * 0.5;

                return new hexCoordinates(u, v);
            }
        }

        function worldCoordinates(x, y) {
            this.x = x;
            this.y = y;
        }

        function hexCoordinates(u, v) {
            this.u = u;
            this.v = v;
        }


    </script>

</head>
<body>

    <canvas id="myCanvas" width=800 height=600/>

</body>
</html>


I won't go much further in this post. I'll just overlay this canvas on top of bing maps to see the result (but changing the grid size and the number of hexagons).


This demo has lots of bugs, mostly related with the zoom operations. Anyway, here's a video showing how the hexagons overlay the map.

I'll eventually fix this demo but in my next post I'm going to do a different approach by creating the hexagons in server-side and generating tiles with that information.

Go to Part 2

No comments:

Post a Comment