Subscribe to GameFromScratch on YouTube Support GameFromScratch on Patreon

25. July 2012

In prior parts we setup a Node.js/Express server to serve a cocos2D HTML project, showed how to host that project in the cloud using Heroku, then we created the backbone of the application itself, providing the ability to populate the app with data, as well as a means to retrieve that data.

 

Now, it’s time to actually create the game itself.

 

As I mentioned at the very beginning, the game is remarkably simple and isn’t really a game at all.  It is a simple series of two configurable pictures, it shows both side by side, then if you click it one zooms in, then if you click again, the next image zooms in, then finally, both images are shown side by side again.  The end result is I can show my daughter a sequence of events so she can better understand cause and effect.

 

Here is a screenshot of our ultimate end result:

image

 

Not earth shattering by any means, but it a) accomplishes what I need to accomplish and can be hosted on any device and accessed anywhere I need it  b) demonstrates all of the core technologies needed to make a much more complex web hosted game or web application.

 

 

Lets jump right in and look at the code.  The graphics are powered by cocos2D HTML, a 2D JavaScript based game library.  You can read some tutorials about it here, as I am not going to get into how cocos2D works in any detail.

 

First we need our AppDelegate.js class, which fires up our cocos2D game.

var cc = cc || {};

cc.AppDelegate = cc.Application.extend({
    ctor:function () {
        this._super();
    },
    initInstance:function () {
        return true;
    },
    applicationDidFinishLaunching:function () {
        var pDirector = cc.Director.sharedDirector();

        var size = pDirector.getWinSize();
        pDirector.setAnimationInterval(1.0 / 60);
        var pScene = FirstThis.scene();
        pDirector.runWithScene(pScene);
        return true;
    },
    applicationDidEnterBackground:function () {
        cc.Director.sharedDirector().pause();
    },
    applicationWillEnterForeground:function () {
        cc.Director.sharedDirector().resume();
    }
});

 

For more information on what is happening here, check the tutorial link I posted earlier.  The most important part to us is the creation of FirstThis, which is the heart of our application.

 

Let’s take a look at FirstThis.js.  Again, I apologize for the wonky formatting, it was done to fit the blog.

 

var FirstThis = cc.LayerColor.extend({
    leftSprite:null,
    rightSprite:null,
    mode:0,
    imageChanged:function(imgName,whichSprite){
        if(this.mode != 0) // We were in transition when select box was changed!
        {
            this.resetVisibility();
            this.mode=0;
        }
        this.removeAllChildrenWithCleanup(true);

        if(this.leftSprite != null && whichSprite=="right")
        {
            this.addChild(this.leftSprite);
        }
        if(this.rightSprite != null && whichSprite=="left"){
            this.addChild(this.rightSprite);
        }

        var imageSize;
        YUI().use('node','io-base',function(Y){
            var results = Y.io("/imageSize/" + imgName, {"sync":true});
            imageSize = JSON.parse(results.responseText);
        });

        var newSpriteWidth = cc.Director.sharedDirector().getWinSize().width/2;
        var newSpriteHeight = cc.Director.sharedDirector().getWinSize().height/2;

        if(whichSprite == "left"){
            this.leftSprite = cc.Sprite.create("/image/" + imgName,
                new cc.Rect(0,0,imageSize.width,imageSize.height));
            this.addChild(this.leftSprite);
            this.leftSprite.setScale(
                (newSpriteWidth * this.leftSprite.getScaleX())/imageSize.width);
            this.leftSprite.setAnchorPoint(new cc.Point(0,1));
            this.leftSprite.setPosition(
                new cc.Point(0,cc.Director.sharedDirector().getWinSize().height));
        }
        else
        {
            this.rightSprite = cc.Sprite.create("/image/" + imgName,
                new cc.Rect(0,0,imageSize.width,imageSize.height));
            this.addChild(this.rightSprite);
            this.rightSprite.setScale(
                (newSpriteWidth * this.rightSprite.getScaleX())/imageSize.width);
            this.rightSprite.setAnchorPoint(new cc.Point(0,1));
            this.rightSprite.setPosition(
            new cc.Point(newSpriteWidth,cc.Director.sharedDirector().getWinSize().height));
        }
    },
    resetVisibility:function()
    {
        this.leftSprite.setIsVisible(true);
        this.rightSprite.setIsVisible(true);
        this.leftSprite.setPosition(
            new cc.Point(0,cc.Director.sharedDirector().getWinSize().height));
        this.rightSprite.setPosition(
                new cc.Point(cc.Director.sharedDirector().getWinSize().width/2,
                cc.Director.sharedDirector().getWinSize().height));
    },
    ctor:function()
    {
        this._super();
    },
    init:function()
    {
        this.setIsTouchEnabled(true);
        this.initWithColor(cc.ccc4(0,0,0,255));

        var that = this;

        YUI().use('node',function(Y){
            Y.one("#firstSel").on("change",function(event){
                if(event.currentTarget.get("selectedIndex") == 0) return;
                    that.imageChanged(event.currentTarget.get("value"),"left");
            });
            Y.one("#thenSel").on("change",function(event){
                if(event.currentTarget.get("selectedIndex") == 0) return;
                    that.imageChanged(event.currentTarget.get("value"),"right");
            });
        });
        this.setAnchorPoint(0,0);
        return this;
    },
    ccTouchesEnded:function (pTouch,pEvent){
        if(this.leftSprite != null && this.rightSprite != null ){
            this.mode++;
            if(this.mode == 1)
            {
                this.leftSprite.setIsVisible(true);
                this.rightSprite.setIsVisible(false);
                this.leftSprite.setPosition(
                        new cc.Point(cc.Director.sharedDirector().getWinSize().width/4,
                        cc.Director.sharedDirector().getWinSize().height));
            }
            else if(this.mode == 2)
            {
                this.leftSprite.setIsVisible(false);
                this.rightSprite.setIsVisible(true);
                this.rightSprite.setPosition(
                    new cc.Point(cc.Director.sharedDirector().getWinSize().width/4,
                        cc.Director.sharedDirector().getWinSize().height));
            }
            else{
                this.resetVisibility();
                this.mode = 0;
            }
        }

    }
});


FirstThis.scene = function() {
    var scene = cc.Scene.create();
    var layer = FirstThis.layer();

    scene.addChild(layer);
    return scene;
}

FirstThis.layer = function() {
    var pRet = new FirstThis();

    if(pRet && pRet.init()){
        return pRet;
    }
    return null;
}

 

Again, most of what is going on here is covered in the earlier tutorial, but I will give a quick overview, and point out some of the application specific oddities as I go.

 

Starting from the top, we declare a pair of variables for holding our two active sprites, as well as a value for the current “mode”, which essentially represents our click state, which will make sense shortly.

 

We then define a method imageChanged which will be called when the user selects a different value in one of the two select boxes at the top of the screen.  On change we first chack to see if the mode is not 0, which means that we are in the process of showing images to the user ( meaning a single image might be visible right now ), in which case we reset visibility and positioning so our two images are side by side again, and reset the mode back to zero.  Then we remove all of the sprites from the layer, effectively erasing the screen.

 

Next we want to check if the user is updating the left or the right image.  If the user is updating the right image for example, we check to see if the left image has been assigned a value yet, and if it has assign it back to the scene.  This test is to prevent trying to push a null sprite onto the scene if the user hasn’t selected images with both drop-downs yet.  We do this for the left and right image.

 

Next we run into a bit of a snag.  This application wants to size images so they each take up 50% of the screen.  There is a problem with this however, as when you declare a sprite with cocos2D, unless you specify the image size, it doesn’t have any dimension data!  This is ok with a game when you will probably know the image dimensions in advance, but in this situation where the images are completely dynamic, it presents a major problem!  This is a problem we solved rather nicely on the server side using node.  We will look at the changes to server.js shortly, but for now realize we add a web service call that returns the specified image dimensions.  We then make a synchronous call using YUI’s io() module to retrieve that information… note the complete lack of error handling here!  My bad.

 

We ideally want to scale our image so their width is half the screen.  We now create our sprite, depending if it is on the left or right side, but the logic is basically identical.  First we create the sprite using the filename passed as a value from our select box, and the dimensions we fetched earlier.  We then add that file to the scene, scale it so the width is 50% of the screen ( scaling it up or down as required ), then position it relative to the top left corner of the sprite, with the X value changing if the sprite is on the left or right.

 

Next up we create the function resetVisibility() which we used earlier on.  Basically it just makes both sprites visible again and puts them back in their default positions.  Next we implement a simple constructor that calls layers constructor.  This is to verify some methods needed to handle touch input are properly called. The cocos2D tutorial on handling input covers this in a bit more detail.

 

Next up is our initialization function, named appropriately enough init().  We tell cocos2D that we are going to handle touch (click) events, that we want to create a layer with a black opaque background.  Next we wire up event handlers to our two select boxes that call our imageChanged() method when, um, and image is changed.  Lastly we tell our layer to anchor using the default bottom left corner.  This call is rather superfluous, as this is the default. I like to include it for peace of mind though, as if there is something I fight with ( and hate! ) about cocos, it’s the coordinate system.

 

Next up we have our touch handler ccTouchesEnded, which will be called when the user taps the screen or when they click a mouse.  This is where the mode variable comes into play.  At a value of 0, it means the screen hasn’t been touched yet, so on first touch, we set the left image to the center of the screen and the right image as invisible.  On the next touch, we do the opposite, then on any further touches we set both images back to their default positions and reset mode back to zero.  The remaining code is simple boiler plate setup code used to create our layer, which again is covered in more detail in these tutorials.

 

In a nutshell, that is our game’s code.  You may remember earlier we made a change to server.js, let’s take a look at the file now.

 

Here is server.js in it’s entirety.  Remember, you do not need to host on Heroku to run this app.  Simple run node server.js from the command line or terminal, then hit localhost:3000 in your web browser.

 

server.js

var express = require('express'),
    server = express.createServer(),
    im = require('imagemagick'),
    files = {};

server.use('/cocos2d', express.static(__dirname + '/cocos2d') );
server.use('/cocosDenshion', express.static(__dirname + '/cocosDenshion') );
server.use('/classes', express.static(__dirname + '/classes') );
server.use('/resources', express.static(__dirname + '/resources') );

server.use(express.bodyParser());

server.get('/', function(req,res){
    res.sendfile('index.html');
    console.log('Sent index.html');
});

server.get('/settings',function(req,res){
   res.sendfile('settings.html');
   console.log('Send settings.html');
});

// API calls
server.get('/image/:name', function(req,res){
    if(files[req.params.name])
    {
        res.contentType(files[req.params.name].contentType);
        res.sendfile(files[req.params.name].path);

        console.log("Returning file" + req.params.name);
    }
});

server.get('/imageSize/:name',function(req,res){
   im.identify(files[req.params.name].path,function(err,features){
       console.log("image/" + req.params.name);
       if(err) throw err;
       else
        res.json({ "width":features.width, "height":features.height });
   });
});

server.get('/getPhotos', function(req,res){
    res.json(files);

});

server.get('/clearAll', function(req,res){
    files = {};
    res.statusCode = 200;
    res.send("");
})

server.post('/upload',function(req,res){
    files[req.files.Filedata.name] = {
        "name":req.files.Filedata.name,
        "path":req.files.Filedata.path,
        "size":req.files.Filedata.size,
        "contentType":req.files.Filedata.type,
        "description":req.body.description };
        console.log(req.files.Filedata);

    console.log(Object.keys(files).length);
    res.statusCode = 200;
    res.send("");
});
server.listen(process.env.PORT || 3000);

Most of this code we have seen before, but there are a couple of new changes.  First thing to notice is:

    im = require('imagemagick'),

We added a new library to the mix to handle image process, the venerable ImageMagick.  You need to install this library before running this code.  First we need to add it to node, that is as simple as, from a command line, cd to your project directory then type:

npm install imagemagick

If you are deploying to Heroku, you also need to update the dependencies in the package.json file, so Heroku is aware of the dependencies.  That file should now look like:

{
    "name": "firstthis",
    "version": "0.0.1",
    "dependencies": {
        "express": "2.5.x",
        "imagemagick":"0.1.x"
    },
    "engines": {
        "node": "0.8.x",
        "npm":  "1.1.x"
    }
}

Finally, you need to install ImageMagick itself.  On Windows the easiest way is to download the binary installer, while for Linux it’s probably easiest to use a package manager of your choice.  Once you install imagemagick, be sure to start a new command line/terminal so it picks up the path variables image magick sets.

 

Ok, now that we have the dependency out of the way, the code itself is trivial:

server.get('/imageSize/:name',function(req,res){
   im.identify(files[req.params.name].path,function(err,features){
       console.log("image/" + req.params.name);
       if(err) throw err;
       else
        res.json({ "width":features.width, "height":features.height });
   });
});

We get file details about the image passed in to the url ( for example with the URL /imageSize/imagename.jpg, name will = imagename.jpg ), then if no errors occur, we return the width and height as a JSON response.

 

 

Finally, we get to the actual HTML, index.html which is served by Node if you request the “/” of the website.

<html>
<head>
 <script src="http://yui.yahooapis.com/3.5.1/build/yui/yui-min.js"></script>
 <script>
  YUI().use('node','io-base',function(Y){

    Y.on("load", function(){
     var canvas = Y.DOM.byId('gameCanvas');
     canvas.setAttribute("width",window.innerWidth-30);
     canvas.setAttribute("height", window.innerHeight-70);
     Y.Get.script(['/classes/cocos2d.js']);

    });

    Y.io('/getPhotos',{
     on: {
      complete:function(id,response){
       var files = JSON.parse(response.responseText);
       var firstSel = Y.DOM.byId("firstSel");
       var thenSel = Y.DOM.byId("thenSel");

       for(var key in files)
       {
           firstSel.options.add(
             new Option(files[key].description,files[key].name));
           thenSel.options.add(
             new Option(files[key].description,files[key].name));
       }
      }
     }
    });

   });
 </script>
</head>
<body style="padding:0; margin: 0; background: black">
 <form>
  <span style="align:left;vertical-align:top;padding-top:0px">
   <label style="color:white;height:40px;font-size:26;vertical-align:middle">
       First:
   </label>
   <select style="height:40px;font-size:22;width:250" id="firstSel">
       <option selected>Drop down to choose</option>
   </select>
   <label style="color:white;height:40px;font-size:26;
   padding-left:10px;vertical-align: middle;">Then:</label>
  <select style="height:40px;font-size:22;width:250" id="thenSel">
    <option>Drop down to choose</option>
   </select>
  </span>
  <span style="float:right;vertical-align: top;margin-top:0px;top:0px;">
   <input align=right type=button value=settings id=settings
       style="height:40px;font-size:26"
      onclick="document.location.href='/settings';"  />
  </span>
 </form>
<div width=100% style="text-align:center;clear:both;">
    <canvas id="gameCanvas">
        Your browser does not support the canvas tag
    </canvas>
</div>
</body>
</html>

Everything here we have seen before, except perhaps this small chunk of extremely important code:

    Y.on("load", function(){
     var canvas = Y.DOM.byId('gameCanvas');
     canvas.setAttribute("width",window.innerWidth-30);
     canvas.setAttribute("height", window.innerHeight-70);
     Y.Get.script(['/classes/cocos2d.js']);

    });

 

This code will be executed once the page finishes loading.  As you may notice, the canvas tag gameCanvas did not have a size specified, as we want it to take the full width of the device it is run on.  Unfortunately you cannot just say width=100% and be done with it, so we set the width and height programmatically when the page loads.  Finally, to make sure that cocos2D objects aren’t loaded and set until after the Canvas tag is resized, I deferred the loading until now, so after we resize the canvas, we dynamically load the cocos2d script, making sure it initializes with the proper dimensions.

 

So, after all our hard work, here is our running completed application in action.  ( Or direct link here, if you don’t want to see it in an iframe ).  Again a warning the data for this application is completely viewer driven, I give no guarantees the content is appropriate! Please be civil ).

 

 

That basically completes the application tutorial.  There are a few issues right now:

1- It doesn’t persist user data.  If the server restarts or a certain period of time elapses, all the data stored on Heroku is lost.  This is easily fixed, but beyond the scope of this tutorial

2- There is a complete lack of hardening or error handling, so the application is probably incredibly fragile

3- There are a few HTML bugs ( oh… aren’t there always? ).  The change event doesn’t always fire when you select the first drop down the first time ( this is a browser bug, and can be worked around, but the code isn’t tutorial friendly. ).  Also, on Safari mobile, cocos2D sprite scaling doesn’t appear to work.

 

 

I hope you found this series useful.  As I was working with git anyways for the Heroku deployment, I decided to make this entire project available on github.  So if you want to fork it and play around, have fun!  I am a massive github newbie though, so don’t be shocked if I screwed something up.  If you, like me, have a child that isn’t doing transitions very well, I hope you find it useful.  I am actually making a more production worth version of this application, so if you need an online first-then board, drop me a line!

 

On a final note, I am going to look into making a PhoneGap version of this application.  If that exercise bares fruit, I will have another section for this tutorial!

Programming , , , ,

24. July 2012

For those of you that are regular readers of this site, you have probably noticed I am a big fan of Safari Books Online, a subscription based book service.  They released a iOS application a year or so back and it was … lacking.  Android subscribers though were left completely in the dark.  There was ( and is ) a mobile version of the site http://m.safaribooksonline.com, but unfortunately it did a horrible job with code examples, which was a pretty big deal.  So, having the mobile app on Android is very nice.

 

It was only just released and I’ve had a small amount of experience with it, and I will say it is vastly improved.  I just wish they would cache books ahead a bit more, as if you flip more than 2 pages you get an annoying “Loading content” display.  The big feature though with the Safari to Go app is the ability to take up to 3 books offline with you.  I have yet to try this feature, but it may remove the annoying loading messages!

 

All told though, it looks like a solid release.

 

I did encounter one oddity though, I couldn’t find it on Google Play.  I had to go the website here, login and then download it.

 

So, if you are a Safari subscriber, be sure to check it out.  If you aren’t, it really is worth looking in to.  If you cannot find it on the store on your device, be sure to try the direct link above.

General

21. July 2012

Now that we have a cocos2D app being served from NodeJS and being hosted in the cloud we now need to get down to the application itself.

 

One of the biggest parts of this app is actually the plumbing behind the scenes.  Essentially I need to be able to upload images to a server and retrieve them on demand.  For each image I need to be able to add a description.  So that is what we are going to create here.

 

If you were ever wondering how to create a full web application using just JavaScript on the client and server, the end result of this exercise is going to show you exactly that.

 

There are a few warnings before hand!

 

1- Heroku doesn’t persist files.  So anything you upload to this example application, it will eventually be lost.  In my actual application I am going to be hosting the actual images to my Dropbox account or using a private Redis database, but in either case, it’s not information I want to share publically.  If you have questions about how I do either, I will gladly answer them.

 

2- THIS ONE IS IMPORTANT!!! I have shared full access to this application to everyone.  That means anyone at any time can upload any picture they want!  So, be aware before you click any links, who knows what you might see!  Also, anyone can delete all the existing images too, so again, don’t expect your uploads to survive long.  It is live for demonstration purposes only, if you want to run something similar, fork it and host your own privately.  Use at your own risk!  GameFromScratch.com takes no responsibility for any content that might be updated.

 

Finally, please be civilized about it.

 

Alright, the warnings and disclaimers out of the way, let’s continue.

 

We are going to take our existing cocos2D audio sample application and add a settings button to the top right corner, like so:

image

 

In order to do so, we are going to make the following changes to Index.html, the file that is served if people request the root of our application:

index.html

<html>
<body style="padding:0; margin: 0; background: #fff;">
<div stype="width:100%;height:50px">
    <form>
        <div align=right>
            <input align=right type=button value=settings id=settings
                  onclick="document.location.href='/settings';"  />
        </div>
    </form>
</div>
<div style="text-align: center; font-size: 0">
    <canvas id="gameCanvas" width="600" height="600">
        Your browser does not support the canvas tag
    </canvas>
</div>
</body>
</html>
<script src="http://cdnjs.cloudflare.com/ajax/libs/jquery/1.7.1/jquery.min.js">
</script>
<script src="classes/cocos2d.js"></script>

 

Really, the only major changes here is the addition of a form, with a button inside with a crude JavaScript redirect to /settings.

 

In our node server application, we now need to handle this new route:

Add the following to server.js

server.get('/settings',function(req,res){
   res.sendfile('settings.html');
   console.log('Send settings.html');
});

This will now cause any requests to /settings to return settings.html.  This document is the webpage where most of the new functionality occurs.  It allows you to select multiple images ( at once using CTRL or SHIFT click ), apply a description to each, and upload it to the server.  It also allows you to review the existing images, as well as clear them all.

 

Let’s take a look at the code. 

settings.html

 

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
        "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
    <title>First This Settings</title>
    <script src="http://yui.yahooapis.com/3.5.1/build/yui/yui-min.js"></script>
    <script>
        YUI().use('uploader', function(Y){
            var uploader =
                new Y.Uploader(
                        {type:"HTML5",
                        uploadURL:"/upload/",
                        multipleFiles:true
                        }
                );

            uploader.render("#selectFiles");

            uploader.after("fileselect", function(event){
                var files = event.fileList;
                var table = Y.one("#filenames tbody");

                if(files.length > 0)
                {
                    Y.one("#buttonUpload").set("disabled",false);
                }

                Y.each(files,function(file){
                    table.append("<tr>" +
                            "<td>" + file.get("name") + "</td>" +
                            "<td>" + file.get("size") + "</td>" +
                            "<td><input type=text id=" + file.get("name") + "></td></tr>");
                });
            });

            uploader.on("alluploadscomplete", function(event) {
                // Hackish page reload
                alert("Files added successfully");
                document.location.href="/settings";
            });

            Y.one("#buttonUpload").on("click", function(){
                if(uploader.get("fileList").length > 0){
                    var perFileVars = {};
                    Y.each(uploader.get("fileList"),function(curFile){
                        perFileVars[curFile.get("id")] =
                        {description:Y.DOM.byId(curFile.get("name")).value};
                    });
                    uploader.set("postVarsPerFile",
                        Y.merge(uploader.get("postVarsPerFile"),perFileVars));


                    uploader.uploadAll();
                }
            });


        });

        YUI().use('io-base','node',function(Y){
            Y.io('/getPhotos',{
                on: {
                    complete:function(id,response){
                        var files = JSON.parse(response.responseText);
                        Y.log(files);
                        var table = Y.one("#existingFiles tbody");
                        for(var key in files)
                        {

                            table.append("<tr>" +
                                    "<td><a href=/image/" + files[key].name + ">"
                                    + files[key].name + "</a></td>" +
                                    "<td>" + files[key].size + "</td>" +
                                    "<td>" + files[key].description + "</td></tr>");
                        }
                    }
                }
            });

            Y.one("#buttonClear").on("click", function(){
                Y.io('/clearAll',{
                    on:{
                        complete:function(id,response){
                            document.location.href="/settings";
                        }
                    }
                });
            });

        });
    </script>
</head>
<body>
<form>
<div style="border:50px">
    <div id=selectFiles>
    </div>
    <div>
        <table id="filenames">
            <thead><tr>
                <th width=350px align=left>File Name</th>
                <th align=left>File Size</th>
                <th align=left>Description</th>
            </tr></thead>
            <tbody></tbody>
        </table>
    </div>
    <div align=right>
        <br /><br />
        <hr />
        <button disabled="true" type="button" id="buttonUpload"class="yui3-button"
                style="width:250px;height:40px;align:right;">Upload Files</button>
    </div>
    <div>
        <br /><hr />
        <table id="existingFiles">
            <thead><tr>
                <th width=150px align=left>File Name</th>
                <th align=left width=60px>File Size</th>
                <th align=left>Description</th></tr></thead>
            <tbody></tbody>
        </table>
    </div>
    <div align=right>
        <br /><br />
        <hr />
        <button type="button" id="buttonHome"class="yui3-button"
                onclick="document.location.href='/'"
                style="width:250px;height:40px;align:right;">Done</button>
        <button type="button" id="buttonClear"class="yui3-button"
                style="width:250px;height:40px;align:right;">Clear All Files</button>
    </div>
</div>

</form>
</body>
</html>

 

Please ignore the wonky formatting, it was done to fit it to this blog width.

As you can see right off the hop, this code makes use of the YUI libraries, a handy set of utilities to perform many common tasks.  In this case I am using them for the Uploader control, as well as for making networked requests back to the server with Y.io.  The YUI libraries are beyond the scope of this document, but fortunately their documentation is extremely good. If you would prefer to use jQuery instead, feel free.

 

In a nutshell, here is what we are doing ( from the top ):

  • create a YUI closure Y, with the uploader module functionality supported
  • create a new uploader, HTML5 style (instead of Flash), set its upload path ( the url it will submit to ) to /upload and finally tell it to support multiple files at once.  This is a big part of why I chose it in the first place, as otherwise with the standard HTML controls, you need to upload files one by one.
  • we then set the uploader to render to the selectFiles div we will define shortly. This simply tells the control where to draw itself.
  • We then wire up an event handler for after the user has selected files. If the user has selected at least one file, we enable the upload button, we then loop through the selected files and dynamically populate the table named filenames.  Perhaps most importantly, for each table row, we create a text field for entering the description, with the filename as it’s id.
  • Next we wire up an event handler for the “alluploadscomplete” event, which is fired predictably enough, when all uploads are complete. In this case we just display a message and redirect to the settings page.
  • We then wire up a click handler for the upload button.  This loops through all of the files select, and grabs the description value we specified in the text field and merges in to the data that is going to be posted back to the server.  Essentially this means the data you enter is each description field is going to be posted along with your files back to the server.  Finally we actually perform the upload by calling uploadAll().
  • Next I create a different YUI closure with different dependencies ( io-base and node ).  I do this simply as personal preference, as I find it makes code easier to move around.  If you preferred, you could have made a single call to YUI().use.
  • This code is basically going to be executed on page load or there abouts ( we don’t really care when ), which makes a network request to /getPhotos, which is a web service that returns a JSON structure of all the photos on the server.  We then loop through the results and display them to another table, existingFiles.
  • next we wire-up another button handler, this one is if the user clicks the Clear All button, which calls the clear all web service, which predictably enough, clears all the photos. Finally it reloads the page, so the tables will be redrawn.
  • Everything else is just standard HTML.

 

There is actually quite a bit of functionality packed in to a fairly small amount of code.  As we saw, we added a number of new functions to the server, lets take a look at them. 

 

server.js

var express = require('express'),
    server = express.createServer(),
    files = {};

server.use('/cocos2d', express.static(__dirname + '/cocos2d') );
server.use('/cocosDenshion', express.static(__dirname + '/cocosDenshion') );
server.use('/classes', express.static(__dirname + '/classes') );
server.use('/resources', express.static(__dirname + '/resources') );

server.use(express.bodyParser());

server.get('/', function(req,res){
    res.sendfile('index.html');
    console.log('Sent index.html');
});

server.get('/settings',function(req,res){
   res.sendfile('settings.html');
   console.log('Send settings.html');
});

// API calls
server.get('/image/:name', function(req,res){
    if(files[req.params.name])
    {
        res.contentType(files[req.params.name].contentType);
        res.sendfile(files[req.params.name].path);

        console.log("Returning file" + req.params.name);
    }
});

server.get('/getPhotos', function(req,res){
    res.json(files);

});

server.get('/clearAll', function(req,res){
    files = {};
    res.statusCode = 200;
    res.send("");
})

server.post('/upload',function(req,res){
    files[req.files.Filedata.name] = {
        "name":req.files.Filedata.name,
        "path":req.files.Filedata.path,
        "size":req.files.Filedata.size,
        "contentType":req.files.Filedata.type,
        "description":req.body.description };
        console.log(req.files.Filedata);

    console.log(Object.keys(files).length);
    res.statusCode = 200;
    res.send("");
});
server.listen(process.env.PORT || 3000);

 

First off, we have added the files var, which is going to hold our file data in memory.  The server.post() handler for upload is probably the most significant change.  This code takes the uploaded form information and populations our files data.  One key addition though was:

server.use(express.bodyParser());

This tells express to install a piece of middleware, that actually processes the form data, and is what makes the /upload method actually have data.

/clearAll is handled by simply erasing our files data, while /getPhotos simply returns that data in JSON format.

 

 

At this point if you are following along at home, you may be wondering how to submit your changes up to heroku.  The process is fairly simple, from a command prompt in your app folder:

heroku login ( give username and password )

git add .

git commit –m “description of changes”

git push master heroku

 

 

In a nutshell these commands a) log you in to heroku b) adds all the new files to your local git repository c) commits the changes you have made into your git repository d) pushes that repository up to heroku. Heroku will automatically restart your app.

 

Here then is our application running on heroku.  Click the settings link in the top right corner to bring up the new settings page we just created.

 

 

In the next part, now that we have a server, a client and some data to work with, we will get down to the actual task of creating our simple game.

 

Here ( assuming it is running ), is our application.  Again, I warn you about whatever content might be live!

General, Programming , , , ,

17. July 2012

 

One of the catches with a web application is finding a place to host it.  If you just have a couple static pages this is pretty simple, there area  few thousand different hosting companies that will serve your pages for a few dollars a month.  However, once you start talking about server side programmability, things get much more complicated.  In this case you generally need a dedicated server or at least, a shared server.  However, the recent move towards cloud computing gives you another option.

 

Such as Heroku.

 

We are now going to look at uploading our cocos2D JavaScript web application to Heroku.  First things first, you need to sign up.  Don’t worry, you don’t need to pay nor give them a credit card, the entry tier is free.

 

Now that you are signed up, we need to make a couple really small alterations to our project, don’t worry, both are pretty minor.  First create a file named package.json

{
    "name": "firstthis",
    "version": "0.0.1",
    "dependencies": {
        "express": "2.5.x"
    },
    "engines": {
        "node": "0.8.x",
        "npm":  "1.1.x"
    }
}

Package.json is a JSON format config file that tells Node what dependencies you have.  You need to tell it what libraries you need ( generally all the things you  “require”ed in code).  The second section is also very important, as it tells what version of Node you want to use.  This is very important with Heroku as if you do not specify a version, you will get Node 0.4.x, which isn’t compatible with our project.  When you run your Heroku app, package.json will determine what is installed.

 

You also need to create a file called Procfile ( with no extension ), like this:

web: node server.js

Think of this like a BAT or SH file that is run on Heroku’s servers when you run it.  You will see this in action shortly. 

 

Now that our application is configured and ready for Heroku, lets get the tools installed.  Head on over to the Heroku toolbelt page and install the version that is right for you.  They have a Windows, Linux and Mac version, all of which should follow basically the same instructions.  For this tutorial I will be using the Windows version, but you should be able to follow along without issue no matter what OS you run.

 

image

 

 

After install completes, open a command prompt ( or terminal… ) and type:

heroku login

Log in using the email address and password you signed up with. If prompted to create an SSH key, allow it.  If you need to generate an SSH key manually, you can do so with the following command:

ssh-keygen –t rsa –C “your@emailaddress.com

You can then tell heroku to use this key with the command:

heroku keys:add

Now it’s time to set up a git repository, which is how we deploy to Heroku.  Don’t worry, its not all that scary.  Again, at the same command line, switch to your application directory ( this part is very important, make sure you cd to your application code directory ) and enter:

git init

git add .

git commit –m “This is a commit comment, put whatever you want”

This combination of commands creates a git repository, adds everything from the current directory and below to it, then commits the changes.  If you look in your directory, you will now notice a .git folder. 

 

Now lets create and deploy our app to heroku. First enter:

heroku create

git push heroku master

If prompted for a security question about trusting heroku, type yes.  These two lines essentially created the app on heroku’s servers, then pushed our git archive to heroku.  At this point, your code is now deployed to Heroku’s servers… almost there!

Now we fire up a copy of our app on their server.  This is where the Procfile we defined earlier comes in.  Essentially you are going to run the command you defined in web: in your Procfile, do this with the command:

heroku ps:scale web=1

Finally, since we are using express, we need to set an environment variable:

heroku config:add NODE_ENV=production

 

And your app is now deployed and live.

 

So… um… where is it?  Ah yeah… type:

heroku apps

image

These are your app server names.  In your case you will have just a single one right now.  Simply take this name and add .herokuapp.com to it.

 

For example, when you open http://radiant-planet-8015.herokuapp.com you see:

image

 

At this point we are running a cocos2D app, served by Node, hosted on Heroku!

 

You can try it out yourself, but I make no promises the server will still be running, so do not consider that URL permanent.  At this point you should be able to deploy your own app to Heroku.

 

The next part is now live.  In this section we will add the ability to populate our application with data.

General , , ,

17. July 2012

 

Google’s cross platform PlayN game library has just released version 1.4.  PlayN enables you to target desktop, iOS, Android, HTML5 and Flash using a single Java code base.

 

 

Details of this release:

 

 

PlayN 1.4 highlights:


- You can now find out when your Android or iOS game is paused and
resumed via PlayN.LifecycleListener.


- WebSocket support has been added (only works on HTML and Java
backends right now).


- Custom GLSL shaders are now supported on all GL-based backends
(HTML5/WebGL, iOS, Android, Java).


- You can use JavaPlatform for your unit tests without having to wire
up the LWJGL native libraries: use JavaPlatform.registerHeadless.


TriplePlay 1.4 highlights:


- Page turn and (iOS-style) flip transitions that use custom shaders
to achieve real 3D perspective (check out the demo on a WebGL-capable
browser: http://threerings.github.com/tripleplay/widgetdemo.html).


- The long awaited particle system has arrived, break out the
fireworks. This currently only works on GL-based backends, but may
some day work on non-GL backends (less performantly).


- There are a couple of not-yet-finished cool new things:

  • a mechanism for syncing persistent storage across multiple clients
    with conflict resolution (so people can play your games on all their
    devices with the same save game state)
  • a player for our Flash animation exporter
    (https://github.com/threerings/flump), so you can export animations
    from the Flash authoring tool and use them in PlayN games

 

Full release notes available here.

 

I have created a number of getting started guides for PlayN, start here for Eclipse and right here for Netbeans.  Of course, be sure to update the version number in either guide.  Once you get through the install process, it’s a very nice library to work with.

News ,

Month List

Popular Comments