Monday, July 16, 2012

Full HTML5 Game Example with Crafty JS

A couple of days ago, I participated in a game jam called "Game Zanga". It was a three-day contest for game development with some conditions: in Arabic, related to a topic announced at the start of the three days (it was "Freedom"), and browser-based.

So it was a nice chance to test my knowledge and experience in game development (which is a new thing to me) and I decided to use Crafty (or maybe some call it CraftyJS). Crafty is a JavaScript game library using HTML5. One great power in Crafty is that it can use Canvas or DOM. Plus the concept of Components and Entities that allow you to develop the game in a OO style.

So, I thought I can post my code for anyone to learn from. Although it is not perfect code and it neads more refactoring and fine-tuning, it is still a good example of how a game can look like in Crafty. I almost did not need anything other than the API documentation on Crafty website (plus the tutorial if you are to start from the very beginning). So with decollapsing the code you'll immediately have a general overview of how the game code looks like, then collapsing the code can do the job to get to the details.

One important note, this game was intended to be an Arabic game, so here are some guidelines:
- The first icon in the home screen is the "Start" button.
- Once the game starts, you have two blue cannons on the left. Click any of them to use it.
- Your abjective is to let the blue soldiers pass ("unrwa" in code) and kill the green ones ("soldier" in code).
- Life points decrease if any green soldier passes.
- Your weapon is upgraded while playing.


Finally, you'll notice some faults in collision detection and a lot of other things in the code, but remember that it was made in only two days by me (a newbie) using Crafty which lacks the variety of full examples to learn from. Anyways, you'll get the idea of a full game in less than 1000 lines and then you can make your own best seller.

Here are some screenshots, followed by a download link.





Download game code here.


Saturday, April 14, 2012

My HTML5 Canvas Profile Widget

Since I've not been innovating much outside work for a while now, I thought I could make any new "thing" to aggregate a few benefits I've been aiming for. The output was this HTML5 canvas, game-like, Egytian-themed profile widget. It was a chance to play with HTML5 canvas, and still stick to the design of the game loop. It was also a chance to change the look of my boring blogs.

You play the game. Drop the egg from the profile you want to visit. If the egg hits the nest, you earn the visit to the profile.

So without any more talking, the code is pretty simple:


  • A game loop
  • Check for user input (mouse clicks)
  • Update the values of some objects
  • Detect collisions of some objects and acting accordingly
  • Draw objects

Notes:
  • The javaScript code is hosted online and only referenced to in my HTML code. This was better to update the code once and apply it to all the places where I use it. (as a desktop/mobile developer, this is great).
  • Loading an image at runtime made a lag of maybe one second when a egg is dropped, but it looked ugly so I commented the code regarding loading the egg images in runtime and used a single preloaded image instead.
  • I've looked about three times for performance boosters. But still this does not mean this is the best to get from HTML5 canvas and Javascript. It is just about the amount of time I gave to this code.
  • If I did not put much comments, it was because I thought the code is self-explaining.


I preferred to post the code here again for better syntax highlighting
[profile_widget_script.js]
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');

var globalFPS               = 20;
var maxItemSpeed            = 2;
var eggSpeed                = 8;
var eggSize                 = 32;
var minItemHeight           = 200;
var iconSize                = 32;
var wingSize                = 32;
var nestX                   = 100;
var nestY                   = 330;
var nestMaxX                = 150;
var nestMinX                = 50;
var nestWidth               = 96;
var nestHeight              = 57;
var nestSpeed               = 2;
var mouseEventFlag          = false;
var mouseEventX             = 0;
var mouseEventY             = 0;
var wave1X                  = -10;
var wave1Y                  = 360;
var wave1Direction          = 1;
var wave2X                  = -25;
var wave2Y                  = 375;
var wave2Direction          = 1;
var wave3X                  = -40;
var wave3Y                  = 390;
var wave3Direction          = 1;
var waveXThreshold          = -50; // where to toggle the direction of wave
var waveSpeed               = 2;

// put the items you want to show here {icon to fly, redirecting url, egg to drop}
var profilesArray = [
            {
            icon:   "http://dl.dropbox.com/u/44163256/html5-profile-widget/assets/facebook_32x32.png",
            url:    "http://www.facebook.com/MahmoudAdly",
            egg:    "http://dl.dropbox.com/u/44163256/html5-profile-widget/assets/egg.png"
            },
            {
            icon:   "http://dl.dropbox.com/u/44163256/html5-profile-widget/assets/twitter_32x32.png",
            url:    "http://www.twitter.com/MahmoudAdly",
            egg:    "http://dl.dropbox.com/u/44163256/html5-profile-widget/assets/egg.png"
            },
            {
            icon:   "http://dl.dropbox.com/u/44163256/html5-profile-widget/assets/gplus_32x32.png",
            url:    "https://plus.google.com/103333225222170744758",
            egg:    "http://dl.dropbox.com/u/44163256/html5-profile-widget/assets/egg.png"
            },
            {
            icon:   "http://dl.dropbox.com/u/44163256/html5-profile-widget/assets/goodreads_32x32.png",
            url:    "http://www.goodreads.com/MahmoudAdly",
            egg:    "http://dl.dropbox.com/u/44163256/html5-profile-widget/assets/egg.png"
            },
            {
            icon:   "http://dl.dropbox.com/u/44163256/html5-profile-widget/assets/linkedin_32x32.png",
            url:    "http://www.linkedin.com/in/MahmoudAdly",
            egg:    "http://dl.dropbox.com/u/44163256/html5-profile-widget/assets/egg.png"
            },
            {
            icon:   "http://dl.dropbox.com/u/44163256/html5-profile-widget/assets/blogger0_32x32.png",
            url:    "http://3adly.blogspot.com/",
            egg:    "http://dl.dropbox.com/u/44163256/html5-profile-widget/assets/egg.png"
            },
            {
            icon:   "http://dl.dropbox.com/u/44163256/html5-profile-widget/assets/blogger1_32x32.png",
            url:    "http://free-3adly.blogspot.com/",
            egg:    "http://dl.dropbox.com/u/44163256/html5-profile-widget/assets/egg.png"
            }
            ];
            
var items = [];
var eggs = [];
var wingImage = new Image;
var nestImage = new Image();
var background = new Image();
var waveImage1 = new Image();
var waveImage2 = new Image();
var waveImage3 = new Image();
var eggImage = new Image(); // loading one egg to use it later, for performance reasons

wingImage.src = "http://dl.dropbox.com/u/44163256/html5-profile-widget/assets/wing_32x32.png";
nestImage.src = "http://dl.dropbox.com/u/44163256/html5-profile-widget/assets/nest.png";
background.src = "http://dl.dropbox.com/u/44163256/html5-profile-widget/assets/background.png";
waveImage1.src = "http://dl.dropbox.com/u/44163256/html5-profile-widget/assets/wave_1.png";
waveImage2.src = "http://dl.dropbox.com/u/44163256/html5-profile-widget/assets/wave_2.png";
waveImage3.src = "http://dl.dropbox.com/u/44163256/html5-profile-widget/assets/wave_3.png";
eggImage.src = "http://dl.dropbox.com/u/44163256/html5-profile-widget/assets/egg.png";

var nest = {
    image: nestImage,
    x: nestX,
    y: nestY,
    width: nestWidth,
    height: nestHeight,
    speed: nestSpeed,
    xDirection: 1,
    minX: nestMinX,
    maxX: nestMaxX
};

createItems();
setInterval(gameLoop, 1000/globalFPS);

function gameLoop(){
    updateUserInput();
    updateItems();
    updateEggs();
    updateNest();
    updateWaves();
    checkEggsCollision();
    drawBackground();
    drawWaves()
    drawItems();
    drawNest();
    drawEggs();
}

function createItems() {
    for(var i in profilesArray) {
        var profile = profilesArray[i];
        var img = new Image();
        img.src = profile.icon;
        
        items.push({
                x: 0- Math.random()*500, // so icons do not appear at once
                y: Math.random()*minItemHeight,
                speed: 1+Math.random()*maxItemSpeed,
                image: img,
                url: profile.url,
                egg: profile.egg,
                wingRotation: 0,
                wingRotationDirection: 1
        });
    }
}

function updateItems() {
    for(var i in items) {
        var item = items[i];
        item.x += item.speed;
        item.wingRotation += item.wingRotationDirection * 0.1
        if(item.wingRotation > Math.PI/4)
        {
            item.wingRotationDirection = -1;
        }
        else if(item.wingRotation < 0)
        {
            item.wingRotationDirection = 1;
        }
        
        // if item gets out of canvas scope
        if(item.x > canvas.width) {
            // recycle the item and set different values instead of removing it
            item.x = 0-Math.random()*100;
            item.y = Math.random()*minItemHeight;
            item.speed = 1+Math.random()*maxItemSpeed //between 2 and 5
            item.wingRotation = 0;
        }
    }
}

function updateEggs(){
    for(var i in eggs){
        var egg = eggs[i];
        egg.y += egg.speed;
        
        // if egg gets out of canvas scope
        if(egg.x > canvas.height) {
                // remove egg from array
                eggs.splice(i, 1);
        }
    }
}

function updateNest(){
    nest.x += nest.xDirection * nest.speed;
    
    if(nest.x > nest.maxX){
        nest.xDirection = -1;
    }
    else if(nest.x < nest.minX){
        nest.xDirection = 1;
    }
}

function updateWaves(){
    wave1X += wave1Direction*waveSpeed;
    if(wave1X > 0)
        wave1Direction = -1;
    else if(wave1X < waveXThreshold)
        wave1Direction = 1;
        
    
    wave2X += wave2Direction*waveSpeed;
    if(wave2X > 0)
        wave2Direction = -1;
    else if(wave2X < waveXThreshold)
        wave2Direction = 1;
    
    wave3X += wave3Direction*waveSpeed;
    if(wave3X > 0)
        wave3Direction = -1;
    else if(wave3X < waveXThreshold)
        wave3Direction = 1;
    
}

function checkEggsCollision(){
    for(var i in eggs){
        var egg = eggs[i];
        if(egg.x+eggSize > nest.x+nest.width/3
            && egg.x < nest.x+2*nest.width/3
            && egg.y+eggSize > nest.y+nest.height/3
            && egg.y < nest.y+2*nest.height/3)
            {
                alert("Thanks for trying my profile widget. You will now be redirected to: \n"
                        + egg.url);
                location.href = egg.url;
                // remove egg from array
                eggs.splice(i, 1);
            }
    }
}

function updateUserInput() {
    if(!mouseEventFlag)
        return;
    
    for(var i in items)
    {
        var item = items[i];
        if( item.x < mouseEventX 
            && mouseEventX < item.x + iconSize
            && item.y < mouseEventY
            && mouseEventY< item.y  + iconSize)
            {
                /* instead of loading the egg in runtime, load it once (eggImage) at startup and use it.
                    Because for a web view, it takes a second to load the image, which looks bad.*/
                //var img = new Image();
                //img.src = item.egg;
                
                eggs.push({
                    x: item.x,
                    y: item.y + iconSize,
                    speed: eggSpeed,
                    image: eggImage,
                    url: item.url
                    });
                mouseEventFlag = false;
                return;
            }
    }
}

function drawBackground() {
    context.drawImage(background, 0, 0);
}

function drawWaves(){
    context.drawImage(waveImage1, wave1X, wave1Y);
    context.drawImage(waveImage2, wave2X, wave2Y);
    context.drawImage(waveImage3, wave3X, wave3Y);
}

function drawItems() {
    for(var i in items) {
        var item = items[i];
        context.drawImage(item.image, item.x, item.y);
        context.save();
        context.translate(item.x+10, item.y+10);
        context.rotate(-item.wingRotation);
        context.drawImage(wingImage, -wingSize, -wingSize);
        context.restore();
    }
}

function drawNest(){
    context.drawImage(nest.image, nest.x, nest.y);
}

function drawEggs() {
    for(var i in eggs) {
        var egg = eggs[i];
        context.drawImage(egg.image, egg.x, egg.y);
    }
}

canvas.onmousedown = function(e) {
    var mousePos = getMousePos(canvas, e);
    mouseEventX = mousePos.x;// - currentTranslationX;
    mouseEventY = mousePos.y;// - currentTranslationY;
    mouseEventFlag = true;
};

canvas.onmouseup = function(e) {
    mouseEventFlag = false;
};

function getMousePos(canvas, evt){
    // get canvas position
    var obj = canvas;
    var top = 0;
    var left = 0;
    while (obj && obj.tagName != 'BODY') {
        top += obj.offsetTop;
        left += obj.offsetLeft;
        obj = obj.offsetParent;
    }
 
    // return relative mouse position
    var mouseX = evt.clientX - left + window.pageXOffset;
    var mouseY = evt.clientY - top + window.pageYOffset;
    return {
        x: mouseX,
        y: mouseY
    };
}

Resources:
HTML5 Canvas Deep Dive
HTML5 Canvas Mouse Coordinates Tutorial

Saturday, February 25, 2012

Qt Performance Tips

As I've been moving away from Qt, I thought I should write some notes about things I've been taking care of when writing a Qt application. Even if they are not the super-duper tips , they may be useful for some people out there.

These tips, and more you may know or apply by default, can make a huge impact on your application. As I remember, the right compination of these tips has helped me to reduce the time of a big operation, and I was shocked by numbers. Imagine a process that takes 7 milliseconds and a smooth UX  instead of 10~30 seconds and a laggy UX!

1- QCoreApplication::processEvents
When getting inside a long loop (for or while loop) the application will freeze because of the intensive resource consumption, so it is advised to process events inside the loops to allow other thread to run. You will notice the difference if, for example, you have a progress bar and you update it from inside the loop. The progress bar UI will not be updated eventhough its values have been set, because the loop is not giving the chance for other processes to work. A line like the following can do the job:
qApp->processEvents();


2- Timer slots instead of threads
Sometimes you want to make a non-blocking process inside your application, so you make another thread, then may get in the hastle of communication with the current thread or modifying the UI. An example I saw and liked was as follows:
QTimer::singleShot(0, this, SLOT(doSomething()));

It was used in a GUI application when there was much code to write in the constructor, and the code was making much delay in the appearence of the main window. So the solution was to move this code to a slot with zero timeout.
Another usage was when querying much data from a database, showing the results of the query simultiniously in the UI (e.g. filling a datasheet line by line) instead of waiting for the query to finish had a great impact on both performance and user experience.


3- QCache
One of the tasks I hate in my application is accessing a database or filesystem. So using the QCache class in my application could minimize the time of accessing a file or database. If you know how computer cache memory works on hardware level you will get the idea. When I query some data from database, I save them in QCache. So the next time I need the data, I look for it inside my cache member before going to the database. If I find it, I use it. I don't find it, I go to the database. This can help boosting the performance if you have an intensive usage of your database or filesystem in runtime (like a game data maybe or history suggestions). So what it the advantage of QCache over QMap or other classes? Here is a quote from the documentation:
When inserting an object into the cache, you can specify a cost, which should bear some approximate relationship to the amount of memory taken by the object. When the sum of all objects' costs (totalCost()) exceeds the cache's limit (maxCost()), QCache starts deleting objects in the cache to keep under the limit, starting with less recently accessed objects.

4- Qt Resource System
"The Qt resource system is a platform-independent mechanism for storing binary files in the application's executable". But it is not always the best choice when you have a lot of files. Remember that this increases the size of your executable, and memory usage. So when you have files that can be placed beside the app instead of beig inside it, this will make less memory usage, leaving more momory for later computations, especially when you have limited hardware resources. Keep the resource system for a minimal number of basic resources like, as the documantation says, a certain set of files (icons, translation files, etc.) that you don't want to run the risk of losing them.


5- Painting
When you paint some graphics inside the application, whether for a game or just a background, it is advisable to take care of the following.
a) You do not always need to repaint all the space. Sometimes, for example, you want to update a logo at the center of your window or splash screen, then why repainting hundreds of thousands of pixels when you only want to repaint a 64x64 rectangle in the middle?
b) When you repaint the same image over and over (for example, a background in the update() method), it is useless to load the image every time in a temporary variable inside the method because loading takes time. It is better to declare a local variable in your header file and load it once (in the constructor or anywhere when needed) then only repaint it without reloading the same image.


6- Database indexing
This may not be a Qt-specific tip, but it is still very important when you have a database of tables of 500,000 entries.
A database index is a data structure that improves the speed of data retrieval operations on a database table at the cost of slower writes and increased storage space. [Wikipedia]
If you do not know about this topic, I suggest you spent an hour reading and applying it on any database to notice the difference it makes.



7- You application in idle state
Some application features are only useful when the user is looking at them. For example when you are retrieving weather conditions from a remote server. What is the benefit of downloading an XML file, parsing, and updating your UI every couple of minutes when the user is not even looking? When the user is in a state when he does not benefit from a feature, it is better to stop/pause it.


So ... these were the performance tips I had in mind till now. I hope you learned something new from them.