Tutorial 1: Create your first realtime multiplayer cross-platform game using Basbosa!

Posted on: 15/11/2012

Prerequisites

Objective

So you want to create games using HTML5? Well good for you! Using HTML5 you can create cross-platform games that run on any device with a decent browser. If you have some web development knowledge, then things should be a lot easier. When creating a game, you should start by a design mock up of game play like this one.

game mockup

You should also have the individual images of each element available as well. Let's create the HTML and CSS for these elements. Most of the elements will be absolutely positioned and later we will manipulate the left and top properties of these elements using JavaScript to move them. Start by creating our old friend index.html.

<!DOCTYPE html>
<html>
<head>
    <title>My First Awesome HTML5 real-time Multiplayer HTML5 Game</title>
    <link class="user" href="css/style.css" rel="stylesheet" type="text/css">
</head>
<body>
<div id="game">
    <div class="ship ship1 my-ship moving bounded"></div>
</div>
</body>
</html>

You can notice that most of the HTML5 code is consumed in the title, but you must be excited doing this so we should celebrate a little bit! In css/style.css we absolutely position our elements.

html, body {
    height : 100%;
    padding : 0;
    margin: 0;
    overflow: hidden;
}
#game {
    background: url('../img/space_bg.jpg');
    height: 100%;
}
.ship {
    position : absolute;
    width : 72px;
    height: 80px;
    top : 300px;
}
.ship1 {
    left: 100px;
    background: url('../img/player1.png') no-repeat;
}
.ship2 {
    left: 200px;
    background: url('../img/player2.png') no-repeat;
}
.fire {
    position : absolute;
    width : 24px;
    height: 45px;
    background: url('../img/fire.png') no-repeat;

}
img {
    border : none;
    position : absolute;
}
.meteor {
    margin-top : -100px;
    top : 0;
    left: 0;
    width : 98px;
    height: 98px;
}
.explode {
    width : 71px;
    height : 100px;
}

The elements should appear now on your browser window. Elements like the fire, meteor and ship2 should only appear dynamically, so they should be removed from the HTML to be like:

<!DOCTYPE html>
<html>
    <head>
        <title>My First Awesome HTML5 real-time Multiplayer HTML5 Game</title>
        <link class="user" href="css/style.css" rel="stylesheet" type="text/css">
    </head>
    <body>
        <div id="game">
            <div class="ship ship1"></div>          
        </div>
    </body>
</html>

Now to the interesting work, let's include Basbosa and start writing some javascript. In the head of your html, under the title tag add:

<script>
var BasbosaConfig = {
    socketUrl   : 'https://dev.hub43.com/'
}
</script>
<script src="https://dev.hub43.com/corec/vendors/require-2.0.4.js"></script>
<script src="https://dev.hub43.com/basbosa.client.js"></script>
<script>require(['js/main']);</script>

What does this do?

  1. BasbosaConfig is an object that I used to bootstrap Basbosa when loaded, here we are just defining the server that we will connect to send and receive game messages
  2. We then include require js http://requirejs.org/ which must be loaded before Basbosa, in the future, requirejs will be bundled with Basbosa so you will not have to add this line, we know how lazy programmers are and we honor that!
  3. Finally, the last line will load your js/main.js file which will contain you custom game code. Note that in requirejs you do not put the file extension of the file you are loading.

Now to the logic of the game. We create a unique id for our player, each player should have a unique id to identify his ship this id is attached to each message sent. We define some game constants.
Basbosa has a built it logger that dumps to the browser console a lot of useful info. The higher the log level the more details that will be provided. We then connect to Basbosa backend server - residing in BasbosaConfig.socketUrl:

define(['app'], function() {
    var myId = (new Date).getTime() + (Math.floor(Math.random() * 1000)), countPlayers = 1,  
    keys = {LEFT : 37, UP : 38, RIGHT : 39, DOWN : 40}, shipVelocity = 4, fireVelocity = 8,
    astroidVelocity = 2, SPACE = 32, SCREEN_WIDTH = 800, SCREEN_HEIGHT = 600;

    $('.my-ship').addClass('player-' + myId);
    Basbosa('Logger').setOptions({level : 4});
    Basbosa('SocketClient').socket.connect();
}

Since this will be a multiplayer game - remember the title of the tutorial? - we have to be efficient in setting and updating the game state which will translate to messages sent to other players. Basically every moving object will have a direction, up, right, left, bottom or none which will translate to the object movement in a certain direction or not moving at all. The direction of a ship is set when a user presses a key and unset when the user releases the key. We will store the direction of each object as data on the DOM object using jquery $.data. When a user changes the direction of his ship, or fires from his ship, we must send a message to other players to update their game state. This is done by calling ltrigger on basbosa, with first parameter the event name and second parameter an object containing the message to be sent. Stick to using ui.public suffices to your message names to reach all users. You will learn more about messaging in Basbosa later.

$(function() {
    $('body').live('keydown', function(e) {
        var keycode = ( e.keyCode ? e.keyCode : (e.which ? e.which : e.charCode));
        for (var direction in keys) {
            if (keycode == keys[direction]) {
                if ($('.my-ship').data('direction') == direction) return;
                $('.my-ship').data('direction', direction);
                Basbosa('j').ltrigger('ui.public.move', {playerId : myId, direction : direction, 
                    position : $('.my-ship').position()});
                break;
            }           
    }
        if (keycode == SPACE) {
            Basbosa('j').ltrigger('ui.public.move', 
                {playerId : myId, direction : $('.my-ship').data('direction'), 
                position : $('.my-ship').position(), fire : true});
                var fire = $('<div>').addClass('fire moving').data('direction', 'UP').css({
                    top : $('.my-ship').position().top + 40,
                    left : $('.my-ship').position().left + 25,
                });
                $('#game').append(fire);
        }
    });

    $('body').live('keyup', function(e) {
        var keycode = ( e.keyCode ? e.keyCode : (e.which ? e.which : e.charCode ));
        for (var direction in keys) {
            if (keycode == keys[direction]) {
                $('.my-ship').data('direction', 'none');
                Basbosa('j').ltrigger('ui.public.move', {playerId : myId, direction : 'none', 
                    position : $('.my-ship').position()});
                break;
            }           
        }
    });
});

We will use the new requestAnimation frame to update the game state. Since each browser has his own version of it, we need the following polyfill. The logic for updating game state will be put in the function animationFrame()

requestAnimFrame = function() {
  result = (
      window.requestAnimationFrame       || 
      window.webkitRequestAnimationFrame || 
      window.mozRequestAnimationFrame    || 
      window.oRequestAnimationFrame      || 
      window.msRequestAnimationFrame     || 
      null
  );
  return result;
}();

if (requestAnimFrame) {
  (function animationLoop() {
        requestAnimFrame(animationLoop);
        animationFrame();               
    })();
} else {
  window.setInterval(animationFrame, 1000 / 60);
}

function animationFrame() {
    $('.moving').each(function() {
        var direction = $(this).data('direction');
        if ($(this).hasClass('ship')) {
            velocity = shipVelocity;
        } else if ($(this).hasClass('fire')) {
            velocity = fireVelocity;
        } else {
            velocity = astroidVelocity;
        }

        if ($(this).hasClass('bounded')) {
            if ($(this).position().left < 10 && direction == 'LEFT') return;
            if ($(this).position().left > SCREEN_WIDTH && direction == 'RIGHT') return;
            if ($(this).position().top < 10 && direction == 'UP') return;
            if ($(this).position().top > SCREEN_HEIGHT && direction == 'DOWN') return;
        }
        switch (direction) {
            case 'LEFT' :
                $(this).css('left', parseInt($(this).css('left')) - (1 * velocity));
                break;
            case 'RIGHT' : 
                $(this).css('left', parseInt($(this).css('left')) + (1 * velocity));
                break;
            case 'UP' : 
                $(this).css('top', parseInt($(this).css('top')) - (1 * velocity));
                break;
            case 'DOWN' : 
                $(this).css('top', parseInt($(this).css('top')) + (1 * velocity));
                break;
        }   
        if ($(this).position().top < -40 || $(this).position().top > 800) $(this).remove();
    });     
};

There are only 3 different types of objects moving on the screen, ships, meteors and fire. Any moving dom object must have the class moving. We loop on all moving objects, get their speed based on their metadata, then move the object based on the object direct. Some objects should be confined inside the screen, they have the class bonded. And finally, if an object goes outside the screen, we should remove it. Now the multiplayer part, messages that are forwarded by the server has the prefix _result. We listen to these messages. If a player who exists in the game universe sends a message, we just update the corresponding dom with the new data from the message. Otherwise we create a new dom object.

Basbosa('SocketClient').lon('public.move_result', function(e, message, next) {
    if (message.playerId == myId) return;

    // If this player is not in out space
    if (!$('.player-' + message.playerId).size()) {
        var $newShip = $('<div>').addClass('ship moving bounded ship' + ++countPlayers);
        $newShip.addClass('player-' + message.playerId);
        $newShip.css('left', message.position.top);
        $('#game').append($newShip);
    }
    $('.player-' + message.playerId).data('direction', message.direction).css({
        top     : message.position.top,
        left    : message.position.left,
    });
    if (message.fire) {
        var fire = $('<div>').addClass('fire moving').data('direction', 'UP').css({
            top : message.position.top + 40,
            left : message.position.left + 25,
        });
        $('#game').append(fire);
    }       
});

To test multiplayer, open two different browsers move your spaceship from one window and you should see it moving from the other window. It should be firing as well. Now let's make things interesting by adding some meteors that are about to hit our planet and destroy humanity. Randomness is a very powerful tool in game design. Because this is a multiplayer game with no server, each user generates a meteor every 3 seconds:

function genAstroids() {
    Basbosa('j').ltrigger('ui.public.meteor', {
        left : Math.floor(Math.random() * SCREEN_WIDTH),
        size : Math.floor(Math.random() * 68) + 30,
        rotation : Math.floor(Math.random() * 360),
    });
}
setInterval(genAstroids, 3000);

And handled:

Basbosa('SocketClient').lon('public.meteor_result', function(e, message, next) {
    var rotate = 'rotate(' + message.rotation + 'deg)', $meteor = $('<img>').attr('src', 'img/meteor.png')
        .addClass('meteor moving')
        .data('direction', 'DOWN')
        .css({left : message.left, width : message.size, height : message.size,
            'transform' : rotate, '-ms-transform' : rotate, '-webkit-transform': rotate,
            '-o-transform' : rotate, '-moz-transform' : rotate
        });
    $('#game').append($meteor);
});

Now you should see few meteors falling down from the sky. You can fire at them but the dam meteors won't explode. This needs to be fixed be a very simple collision detection between meteors and fire objects inside the animationFrame function. We loop on these all meteors to see it they are about to explode by the furious fire coming from our spaceships protecting earth. We remove the meteor and at the position of the metoer we add a simple gif showing the explosion.

$('.meteor').each(function(){
        var $meteor = $(this), mp = $meteor.position();
        $('.fire').each(function() {
            var $fire = $(this), fp = $fire.position();
            if (fp.left >= mp.left && fp.left <= mp.left + $meteor.width()) {
                if (mp.top > fp.top + $meteor.height()) {
                    $fire.remove();
                    $meteor.remove();
                    var explode = $('<img>').attr('src', 'img/explode.gif').css({
                            top : mp.top - 30,
                            left : mp.left
                    });
                    $('#game').append(explode);
                    setTimeout(function(){
                        explode.remove();
                    }, 1000);
                }
            }
        });
    });