+40 745 232 788

Online Solutions Development Blog   |  

RSS

3D board game in a browser using WebGL and Three.js, part 3

posted by ,
Categories: JavaScript, Web Development
Taggs , ,

Continuing from part 2 this final article will show you how to make the pieces move by drag and drop and adding some game logic.

WebGL 3d Checkers Board - pieces dragged

Note: Unless specified otherwise, the file you need to edit will be BoardController.js.

Restrict camera rotation

The first thing we need to do is to stop the camera from moving if the left mouse button is pressed on the board area. We’ll have to create a few functions to accomplish that. Let’ start by creating an initListeners function in which we’ll listen for mouse events:

function initObjects(callback) { ... }

function initListeners() {
    var domElement = renderer.domElement;

    domElement.addEventListener('mousedown', onMouseDown, false);
    domElement.addEventListener('mouseup', onMouseUp, false);
}

Call the new function from drawBoard, after initObjects. After onAnimationFrame function definition add the mouse event handlers:


function onMouseDown(event) {
    var mouse3D = getMouse3D(event);

    if (isMouseOnBoard(mouse3D)) {
        cameraController.userRotate = false;
    }
}

function onMouseUp(event) {
    cameraController.userRotate = true;
}

On line 2 the mouse 2D coordinates are converted to a 3D position. On line 4 we check if the mouse was pressed on the board and if true, the camera rotation is disabled on line 5. On mouse up the camera rotation will be re-enabled. To calculate the 3D mouse position we’ll need a THREE.Projector instance, so let’s create one in initEngine function:

...
var renderer;
var projector;
...

function initEngine() {
    ...
    renderer.setSize(viewWidth, viewHeight);

    projector = new THREE.Projector();
    ...
}

Now we’re ready to define our getMouse3D function after boardToWorld:

function getMouse3D(mouseEvent) {
    var x, y;
    //
    if (mouseEvent.offsetX !== undefined) {
        x = mouseEvent.offsetX;
        y = mouseEvent.offsetY;
    } else {
        x = mouseEvent.layerX;
        y = mouseEvent.layerY;
    }

    var pos = new THREE.Vector3(0, 0, 0);
    var pMouse = new THREE.Vector3(
        (x / renderer.domElement.width) * 2 - 1,
       -(y / renderer.domElement.height) * 2 + 1,
       1
    );
    //
    projector.unprojectVector(pMouse, camera);

    var cam = camera.position;
    var m = pMouse.y / ( pMouse.y - cam.y );

    pos.x = pMouse.x + ( cam.x - pMouse.x ) * m;
    pos.z = pMouse.z + ( cam.z - pMouse.z ) * m;

    return pos;
}

What I can say about the getMouse3D function is that the value for Y axis will always be 0 since we just need to know where the mouse was clicked in the XZ plane space. You can just take the algorithm that translates 2D position to a 3D one as it is, it’s not my creation. The isMouseOnBoard function is defined after getMouse3D like this:

function isMouseOnBoard(pos) {
    if (pos.x >= 0 && pos.x <= squareSize * 8 &&
        pos.z >= 0 && pos.z <= squareSize * 8) {
        return true;
    } else {
        return false;
    }
}

We just check if the mouse event happened inside the board’s squares area. If you test in the browser now you’ll see that you can only manipulate the camera by dragging outside the board (or on its margins).

Dragging the pieces

To drag the pieces around we need to check if the mouse was pressed on a piece in the mouse down event handler. If the mouse was on a piece (actually on a piece square) we need to select it and to start listening for mouse move event. Let’s modify the onMouseDown function:

...
if (isMouseOnBoard(mouse3D)) {
    if (isPieceOnMousePosition(mouse3D)) {
        selectPiece(mouse3D);
        renderer.domElement.addEventListener('mousemove', onMouseMove, false);
    }

    cameraController.userRotate = false;
}
...

Add the isPieceOnMousePosition function after isMouseOnBoard:

function isPieceOnMousePosition(pos) {
    var boardPos = worldToBoard(pos);

    if (boardPos && board[ boardPos[0] ][ boardPos[1] ] !== 0) {
        return true;
    }

    return false;
}

First the 3D position must be converted to the internal board position on line 2. On the 4th line we check if there’s an actual piece in the position we found. Define the worldToBoard function after boardToWorld like this:

function worldToBoard(pos) {
    var i = 8 - Math.ceil((squareSize * 8 - pos.z) / squareSize);
    var j = Math.ceil(pos.x / squareSize) - 1;

    if (i > 7 || i < 0 || j > 7 || j < 0 || isNaN(i) || isNaN(j)) {
        return false;
    }

    return [i, j];
}

Now we need to define the selectPiece function after isPieceOnMousePosition. A new property also needs to be declared:

...
var board = [...];

var selectedPiece = null;
...
function isPieceOnMousePosition(pos) {...}

function selectPiece(pos) {
    var boardPos = worldToBoard(pos);

    // check for piece presence
    if (board[ boardPos[0] ][ boardPos[1] ] === 0) {
        selectedPiece = null;
        return false;
    }

    selectedPiece = {};
    selectedPiece.boardPos = boardPos;
    selectedPiece.obj = board[ boardPos[0] ][ boardPos[1] ];
    selectedPiece.origPos = selectedPiece.obj.position.clone();

    return true;
}

Besides storing the actual piece object, we need to store its position also to be able to make a move or to put it back where it was on illegal move.

Define the onMouseMove function like this:


function onMouseMove(event) {
    var mouse3D = getMouse3D(event);

    // drag selected piece
    if (selectedPiece) {
        selectedPiece.obj.position.x = mouse3D.x;
        selectedPiece.obj.position.z = mouse3D.z;

        // lift piece
        selectedPiece.obj.children[0].position.y = 0.75;
    }
}

If you try to move a piece now you’ll notice that it’s sticking to the mouse cursor even after mouse up release. Let’s fix that onMouseUp:

function onMouseUp(event) {
    renderer.domElement.removeEventListener('mousemove', onMouseMove, false);

    var mouse3D = getMouse3D(event);

    if (isMouseOnBoard(mouse3D) && selectedPiece) {
        var toBoardPos = worldToBoard(mouse3D);

        if (toBoardPos[0] === selectedPiece.boardPos[0] && toBoardPos[1] === selectedPiece.boardPos[1]) {
            deselectPiece();
        } else {
            instance.movePiece(selectedPiece.boardPos, toBoardPos);
            selectedPiece = null;
        }
    } else {
        deselectPiece();
    }

    cameraController.userRotate = true;
}

If an illegal move is made the deselectPiece function will reset the piece position, otherwise the movePiece will place the piece to a new position. Here’s how deselectPiece looks like:

function selectPiece(pos) {...}

function deselectPiece() {
    if (!selectedPiece) {
        return;
    }

    selectedPiece.obj.position = selectedPiece.origPos;
    selectedPiece.obj.children[0].position.y = 0;

    selectedPiece = null;
}

The movePiece method we want it to be public, so we’ll have to store the instance of the BoardController to be accessible from inside the mouse event handlers:

...
options = options || {};

var instance = this;
...

this.addPiece = function (piece) {...};

this.movePiece = function (from, to) {
    var piece = board[ from[0] ][ from[1] ];
    var capturedPiece = board[ to[0] ][ to[1] ];
    var toWorldPos = boardToWorld(to);

    // update internal board
    board[ from[0] ][ from[1] ] = 0;
    delete board[ to[0] ][ to[1] ];
    board[ to[0] ][ to[1] ] = piece;

    // capture piece
    if (capturedPiece !== 0) {
        scene.remove(capturedPiece);
    }

    // move piece
    piece.position.x = toWorldPos.x;
    piece.position.z = toWorldPos.z;

    piece.children[0].position.y = 0;
};

While moving the piece, we also remove the piece from the destination position if necessary. Now you can try moving the pieces in the browser. You should be able to move them freely on every square you want and if you drop them outside the board area the pieces will be placed back to their initial position.

Adding game logic

The checkers game logic will reside in Game.js, so we’ll have to make the BoardController notify the Game when a piece needs to be/or have dropped. We’ll make that happen by allowing some callbacks to be registered when the BoardController is instantiated:

..
var board = [...];
var selectedPiece = null;

var callbacks = options.callbacks || {};
...

Now let’s make use of those callbacks in onMouseUp function:

function onMouseUp(event) {
    renderer.domElement.removeEventListener('mousemove', onMouseMove, false);

    var mouse3D = getMouse3D(event);

    if (isMouseOnBoard(mouse3D) && selectedPiece) {
        var toBoardPos = worldToBoard(mouse3D);

        if (toBoardPos[0] === selectedPiece.boardPos[0] && toBoardPos[1] === selectedPiece.boardPos[1]) {
            deselectPiece();
        } else {
            if (callbacks.pieceCanDrop && callbacks.pieceCanDrop(selectedPiece.boardPos, toBoardPos, selectedPiece.obj.color)) {
                instance.movePiece(selectedPiece.boardPos, toBoardPos);

                if (callbacks.pieceDropped) {
                    callbacks.pieceDropped(selectedPiece.boardPos, toBoardPos, selectedPiece.obj.color);
                }

                selectedPiece = null;
            } else {
                deselectPiece();
            }
        }
    } else {
        deselectPiece();
    }

    cameraController.userRotate = true;
}

Instead of just moving the piece, on line 12 the pieceCanDrop callback is called to check if the piece can be dropped on the requested square. If returns true the piece is moved and the pieceDropped callback is called to allow the Game to remove the captured pieces.

Now let’s add some game logic into Game.js. We’ll start by creating a variable to keep track of which side’s turn is and by modifying the init function to pass some callbacks to BoardController instance:

...
var board = [...];

var colorTurn = CHECKERS.WHITE;

function init() {
    boardController = new CHECKERS.BoardController({
        containerEl: options.containerEl,
        assetsUrl: options.assetsUrl,
        callbacks: {
            pieceCanDrop: isMoveLegal,
            pieceDropped: pieceMoved
        }
    });

    boardController.drawBoard(onBoardReady);
}

Here’s how the isMoveLegal function looks like:

function onBoardReady() {...}

function isMoveLegal(from, to, color) {
    if (color !== colorTurn) {
        return false;
    }

    var fromRow = from[0];
    var fromCol = from[1];
    var toRow = to[0];
    var toCol = to[1];

    if (board[toRow][toCol] !== 0) { // a piece can't be dropped on an existing piece
        return false;
    }

    if (color === CHECKERS.BLACK) {
        // checks for one square move in left or right direction
        if (toRow === fromRow + 1 && (toCol === fromCol - 1 || toCol === fromCol + 1)) {
            return true;
        }

        // checks for 2 squares move (jumping over a piece)
        if (toRow === fromRow + 2) {
            // left direction
            if (toCol === fromCol - 2 && board[fromRow + 1][fromCol - 1] !== 0 && board[fromRow + 1][fromCol - 1].color != color) {
                return true;
            }

            // right direction
            if (toCol === fromCol + 2 && board[fromRow + 1][fromCol + 1] !== 0 && board[fromRow + 1][fromCol + 1].color != color) {
                return true;
            }
        }
    } else if (color === CHECKERS.WHITE) {
        // checks for one square move in left or right direction
        if (toRow === fromRow - 1 && (toCol === fromCol - 1 || toCol === fromCol + 1)) {
            return true;
        }

        // checks for 2 squares move (jumping over a piece)
        if (toRow === fromRow - 2) {
            // left direction
            if (toCol === fromCol - 2 && board[fromRow - 1][fromCol - 1] !== 0 && board[fromRow - 1][fromCol - 1].color != color) {
                return true;
            }

            // right direction
            if (toCol === fromCol + 2 && board[fromRow - 1][fromCol + 1] !== 0 && board[fromRow - 1][fromCol + 1].color != color) {
                return true;
            }
        }
    }

    return false;
}

The function restricts a piece movement only to diagonal jumping by 1 or 2 squares. In the case of a 2 squares jump there must be an enemy piece in the middle.

Now here’s the pieceMoved function:


function pieceMoved(from, to, color) {
    var fromRow = from[0];
    var fromCol = from[1];
    var toRow = to[0];
    var toCol = to[1];

    board[toRow][toCol] = board[fromRow][fromCol];

    board[fromRow][fromCol] = 0;

    // capture jumped piece
    if (toRow === fromRow - 2) { // left direction
        if (toCol === fromCol - 2) {
            boardController.removePiece(fromRow - 1, fromCol - 1);
            board[fromRow - 1][fromCol - 1] = 0;
        } else {
            boardController.removePiece(fromRow - 1, fromCol + 1);
            board[fromRow - 1][fromCol + 1] = 0;
        }
    } else if (toRow === fromRow + 2) { // right direction
        if (toCol === fromCol + 2) {
            boardController.removePiece(fromRow + 1, fromCol + 1);
            board[fromRow + 1][fromCol + 1] = 0;
        } else {
            boardController.removePiece(fromRow + 1, fromCol - 1);
            board[fromRow + 1][fromCol - 1] = 0;
        }
    }

    // change turn
    if (color === CHECKERS.WHITE) {
        colorTurn = CHECKERS.BLACK;
    } else {
        colorTurn = CHECKERS.WHITE;
    }
}

This function removes a captured (jumped) piece and toggles the turn to move. In order to remove a captured piece the BoardController needs one more method:


this.addPiece = function (piece) {...};

this.removePiece = function (row, col) {
    if (board[row][col]) {
        scene.remove(board[row][col]);
    }

    board[row][col] = 0;
};

That’s all folks! If you want to experiment more you could add some improvements to the game, like complete game logic or making the captured pieces fade out using tween.js.

You can download the sample project from here.

If you have questions, suggestions or improvements don’t hesitate to add a comment and let me know about them.

If you liked this post
you can buy me a beer

3 Responses to 3D board game in a browser using WebGL and Three.js, part 3

Leave a Reply to mishu Cancel reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

Also, if you want to display source code you can enclose it between [html] and [/html], [js] and [/js], [php] and [/php] etc