Subscribe to GameFromScratch on YouTube Support GameFromScratch on Patreon

12. October 2012

 

I have been following RIM’s Gameplay 3d engine for some time now and they have been improving it at a pretty impressive rate.  Well, they just announced the release of version 1.5.  If you’ve never heard of Gameplay, it is a cross platform mobile game development kit written in C++, but scriptable in Lua.

 

Gameplay3d currently supports the following targets:

  • BlackBerry 10 and PlayBook 2.0 (using BlackBerry Native SDK)
  • Apple iOS 5.1 (using Apple XCode 4.3.2)
  • Google Android 2.3+ (using Google Android NDK, SDK API level 9+)

 

And can be developed using the following platforms/compilers:

  • Microsoft Windows 7 (using Microsoft Visual Studio 2010)
  • Apple MacOS X (using Apple XCode 4.3.2)
  • Linux (using CMake or CodeBlocks 10)

 

As to what’s new in 1.5:

  • Linux support. (tested on Ubuntu 12)
  • CMake support for makefile generation for Linux.
  • CodeBlocks 10 IDE support for Linux.
  • Gamepad controllers support for desktops.
  • Touch gesture support for tap, swipe and pinch.
  • Vehicle phyics support via new PhysicsVehicle and PhysicsVehicleWheel classes.
  • Adds new racer sample (sample06-racer).
  • Adds gameplay-tests project as a test app for various basic engine features with some initial tests.
  • Adds support for Scene files for wildcard identifiers.
  • Adds Visual Studio Plug-in support for BlackBerry PlayBook and BlackBerry 10.
  • Adds configurable multi-sampling anti-aliasing support.
  • Adds updates to latest FBX SDK 2013.3.
  • Adds file formats documenation for game.config .scene, .material, .animation, .physics, .particle
  • Adds Game/Platform::canExit for testing device capabilities to quit. (only ios)
  • Web community forums at http://www.gameplay3d.org/forums.
  • Changed keyTimes from unsigned long[] to unsigned int[]. (breaks compat. in AnimationTarget and Animation::Channel)
  • Fixed inconsistencies from Bundle::getObjectID() to Bundle::getObjectId() (breaks compat. in Bundle)
  • Fixes the texture coordinates of Mesh::createQuad(float x, float y, float width, float height).
  • Fixes line-wise distortion when loading RGB png's into texture that are non-power of two.
  • Fixes inconsitencies in createXXXX methods. (breaks compat. in Scene)
  • Fixes Rectanngle::contains.
  • Fixes Lua print logging.
  • Fixes Lua errors to be treated as runtime warnings.
  • Fixes setVertexData to pointers instead of constant data.
  • Fixed AudioSource so that it doesn't loop by default.
  • Fixes minor UI scrolling issues.

 

And what’s in the works:

  • AI Pathfinding
  • Terrain and Water
  • Asset Pipeline improvements

 

You can download Gameplay on Github.  For a quick guide on getting started check here, it’s a bit old but should still be mostly relevant.

 

Good work Gameplay team!

News , ,

12. October 2012

 

If you followed my Moai tutorial series, you may recall in this post I mentioned that Android keyboard support currently didn’t exist, although there is an effort underway to fix that.  Well, a user over on the Moai forums got sick of waiting and implemented a soft keyboard using MoaiGui, which I covered in this post.

 

It is written as a Lua module and the only dependency is you need to pass in your Moaigui object.  Here is the code:

--utility function
function distance ( x1, y1, x2, y2 )
        return math.sqrt ((( x2 - x1 ) ^ 2 ) + (( y2 - y1 ) ^ 2 ))
end

k = {}

local kboard = nil
local keysWide = 1
local keySize = 5
local boardHeight = keySize
local left = 50 - keySize/2
local top = 100
local xpos = left
local ypos = top
local rowCount = 0
local speed = 1000
local textbox = nil

--release all gui objects
k.destroyKeyboard = function (self, gui)
        if kboard ~= nil then
                for k, v in pairs(kboard) do
                        gui:destroyWindow(v)
                end
                kboard = nil
        end
end

--setup a new keyboard, starting with only 1 row
k.createKeyboard = function (self, gui, charsWide)
        if kboard ~= nil then destroyKeyboard(gui) end
        kboard = {}
        keysWide = charsWide
        boardHeight = keySize
        left = 50 - keySize*charsWide/2
        xpos = left
        top = 100
        ypos = top
        rowCount = 0
end

 

local function keyClick(event, data)
        --give focus to selected textbox so that it receives the keypress
        if textbox ~= nil then data.g:setFocus(textbox) end
        --pass keypress to gui
        data.g:injectKeyDown(data.character)
        data.g:injectKeyUp(data.character)
end

 

k.addKey = function (self, gui, char)
        if kboard == nil then return end
        --empty string indicates skipping a key slot
        if char ~= "" then
                kboard[char] = gui:createButton()
                kboard[char]:setPos(xpos, ypos)
                kboard[char]:setDim(keySize, keySize)
                kboard[char]:setText(char)
                data = {}

                data.g = gui
                if char == "<" then
                        data.character = 8 --backspace
                else
                        data.character = string.byte(char) --ascii value of character
                end

                kboard[char]:registerEventHandler(kboard[char].EVENT_BUTTON_CLICK, nil, keyClick, data)
        end
        --increment to next key in row
        rowCount = rowCount + 1
        xpos = xpos + keySize
        if rowCount >= keysWide then
                --new row
                xpos = left
                ypos = ypos + keySize
                boardHeight = boardHeight + keySize
                rowCount = 0
        end
end

 

--moves the key to its place at the bottom of the screen (or off the screen if show=false).
--NOTE: only returns once the move has finished
local function moveKey(key, show)
        local x, y = key:getPos()
        --move onto the screen
        local newy = y - boardHeight
        --move off the screen
        if not show then newy = y + boardHeight end
        --not really the best way of getting the target location, but the easiest/quickest solution
        key:setPos(x, newy)
        local tx, ty = key._rootProp:getLoc()
        key:setPos(x,y)
        --calculate a travel time relevant to the distance being traveled
        local travelTime = distance(x, y, x, newy) / speed

        MOAIThread.blockOnAction(key._rootProp:seekLoc(tx, ty, travelTime, MOAIEaseType.LINEAR))
        --the seekLoc only moves the prop, the prop container is not aware of the move, so tell it.
        key:setPos(x, newy)
end

 

--moves all keys to the desired location
k.showKeyboard = function (self, gui, show)
        if kboard == nil then return end
        --only need to pass in show when you want to hide the keyboard
        if show == nil then show = true end
        for k, v in pairs(kboard) do
                --move keys in separate threads
                MOAIThread.new():run(moveKey, v, show)
        end
end

 

k.hideKeyboard = function (self, gui)
        self:showKeyboard(gui, false)
end

k.setTextbox = function(self, tbox)
        textbox = tbox
end

return k

 

 

And here is some code demonstrating the keyboard in action:

 

local kb = require "keyboard"

kb:createKeyboard(gui, 11)
keys = {"1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "<",
        "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "",
        "", "a", "s", "d", "f", "g", "h", "j", "k", "l",  "",
        "", "", "z", "x", "c", "v", "b", "n", "m"}
for k, v in pairs(keys) do
        kb:addKey(gui, v)
end

kb:showKeyboard(gui)

local editBox = nil
local function handleEditBoxGainFocus(self, event)
        self._cursorPos = #self._internalText + 1
        self:_addCursor()
        kb:setTextbox(editBox)
        return self:_baseHandleGainFocus(event)
end

editBox = gui:createEditBox()
editBox :setPos(40, 30)
editBox :setDim(20, 5)
editBox :setText("")
editBox ._onHandleGainFocus = handleEditBoxGainFocus

 

 

None of this code is mine, all of the credit goes to lancew over on the Moai forums.  The only thing I have done is slightly changed the formatting to make it a bit more legible. You can read the original thread right here.

 

Great work Lance!

Programming ,

12. October 2012

Just a heads up, this post has absolutely nothing to do with game development.  It was a little annoyance I ran into today that had a completely unintuitive answer.  I post here so that, thanks to the power of Google, others may learn from my frustration.

 

I have a laptop that is for portability, the one I use when I am out and about ( which is more often then not ), but when I am home, my wife often makes use of it.  For debugging reasons, I hate having unknown processes running in the background, so I always log her account off the next time I log in.  I have done it the same way for years, open Task Manager ( CTRL+SHIFT+ESC ), go to the Users tab, right click the user name and select log off.  As the title suggests, today things didn’t quite work out and I received the error “User username (SessionId=1) could not be logged off.  Access denied.”.  First off, my wife’s name isn’t Bob in case you are wondering.

 

Second, the fix is really easy, just completely confusing why it is a fix at all!

In Task Manager, switch over to the process tab and click “Show processes from all users.”

 

image

 

That’s it, you can now log the other user off.

 

Again, I haven’t got the foggiest idea why this works.  I have that feature checked 99.99% of the time, so I have never run into this problem before.  Obviously more happens behind the scenes than just displaying more processes when you click that button.

 

Anyways, hope that proves useful to someone at some point. 

 

Now back to our regularly scheduled programming.

Totally Off Topic

11. October 2012

Our application hasn’t looked very… applicationy up until this point.  The menu area was basically a space full with “coming soon”.  In this post we will address adding a menu to our HTML app and show how we can pass fire and handle menu click events.

 

First change I suppose, we need a menu.  We will be using the YUI menu plugin MenuNav.  If I am honest, it is unweildy compared to some HTML UI widgets I have used in the past, but since we are using YUI, might as well use it for everything.

 

We make the following changes to mainMenu.Template

 

<div style="width:100%" class="yui3-skin-sam">
    <div id="appmenu" class="yui3-menu yui3-menu-horizontal"><!-- Bounding box -->
        <div class="yui3-menu-content" ><!-- Content box -->
            <ul>
                <li>
                <a class="yui3-menu-label" href="#file">File</a>
                <div id="file" class="yui3-menu">
                    <div class="yui3-menu-content">
                        <ul>
                            <li class="yui3-menuitem" id="menuFileExit">
                                <a class="yui3-menuitem-content" href="#">Exit</a>
                            </li>
                        </ul>
                    </div>
                </div>
                </li>
            </ul>
        </div>
    </div>
</div>

Read the link above for more details about exactly what is going on here.  The key things to notice are the id’s for the menu (appmenu) and menu item (menuFileExit), both of those will be used shortly.  It is also of key importance to give the containing div the class yui3-skin-sam, as this is what brings in all of the YUI3 css and formatting.  You could also add this to the <BODY> tag in editor.View.js, which we may do as we add more YUI controls.  Just be aware that a parent node within the DOM needs to have this class declared.

 

So, that’s is our markup, lets look at the code side of things.  Open up and change mainMenu.View.js

YUI.add('mainMenuView',function(Y){
    Y.MainMenuView = Y.Base.create('mainMenuView', Y.View, [], {
        initializer:function(){
            var results = Y.io('/scripts/views/templates/mainMenu.Template',{"sync":true});
            // No need to compile, nothing in template but HTML 
            // this.template = Y.Handlebars.compile(results.responseText);
            this.template = results.responseText;
        },
        render:function(){
            this.get('container').setHTML(this.template);
            var container = this.get('container');

            var menu = container.one("#appmenu");
            menu.plug(Y.Plugin.NodeMenuNav);

            //Register menu handlers
            var menuFileExit = container.one('#menuFileExit');

            menuFileExit.on("click",function(e){
                alert("Publishing");
                Y.Global.fire('menu:fileExit', {
                    msg:"Hello"
                });
            });

            var menuFileAddSpriteSheet = container.one('#menuFileAddSpriteSheet');
            menuFileAddSpriteSheet.on("click", function(e){
                Y.Global.fire('menu:fileAddSpriteSheet', {msg:null});
            });

            return this;
        }
    });
}, '0.0.1', { requires: ['view','io-base','node-menunav','event','handlebars']});

Here we changed our initializer to load synchronously as well, otherwise the basics are pretty much the same.  Not that we added the ‘node-menunav’ and ‘event’ dependencies to our requires array.  Otherwise the key changes are:

var menu = container.one("#appmenu");
menu.plug(Y.Plugin.NodeMenuNav);

This locates our appmenu div and plugs the NodeMenuNav into it, turning our DIV into a YUI3 style menu.  Basically this is where the magic happens.  Then:

var menuFileExit = container.one('#menuFileExit');
menuFileExit.on("click",function(e){
    alert("Publishing");
    Y.Global.fire('menu:fileExit', {
        msg:"Hello"
    });
});

Next we find our menuFileExit menu item and register an onClick handler for it.  When a click occurs we fire a global event named “menu:fileExit”, with a msg of Hello.  The name menu:fileExit was chosen by me and can be anything.  So, when the user clicks the Exit item in the menu, this event will be fired.  Let’s look at how you handle “catching” this event.  Open up editor.View.js and at the bottom of the initializer() function, add the following code:

Y.Global.on('menu:fileExit', function(e){
   alert(e.msg);
});

Basically, this monitors for a menu:fileExit event being fired, and simple alerts the contents.  This illustrates a simple way to provide a global menu which can be handled across multiple views.

 

Here is our project in action now:

Basically, it is exactly the same as before, but now it has a menu.

 

You can download the complete source code here.

Design, Programming , , ,

9. October 2012

As we saw in the last part, our application is made up of a single view composed of 3 child views.  In this post I am going to focus on the left hand view, which is where the actual map will be drawn.  This is easily the most important part.

 

All I hoped to accomplish today was to get an EaselJS stage integrated in to a YUI View, which with some horrific hacking, I have accomplished.  There are a few very important requirements.

 

First, we need to have a canvas element that EaselJS can work with.

Second, we want the canvas element to take up as much room on the UI as possible.  The right hand view is going to be fixed at 280 pixels in width, so we want the map editing area to consume the rest of the screen.

Finally, I want the whole thing to resize if the window is resized, so our application can support any resolution.

 

To accomplish this, I have altered the person.Template ( the right hand side placeholder for now ), to look like this:

<div style="width:280px;min-width:280px;max-width: 280px;float:right">
    <div align=right>
        <img src="http://www.gamefromscratch.com/image.axd?picture=HTML-5-RPG_thumb_1.png" 
             alt="GameFromScratch HTML5 RPG logo" />
    </div>
    <p><hr /></p>
    <div>
        <h2>About {{name}}:</h2>
        <ul>
            <li>{{name}} is {{height}} feet tall and {{age}} years of age.</li>
        </ul>
    </div>
</div>

Only real change here is the alteration to the parent div.

 

In editor.View.js I made the following simple change that the bottom of the render() function:

Y.one('body').setStyle("margin",0);
return this;

This is simply overriding the YUI default BODY styling, as I do not want any margins, padding or spaces between elements.

 

Then I altered map.Template as follows:

<div style="margin:0px;float:left;display:block" id="panel">
    <canvas width=300 height=300 id="mainCanvas" style="background-color: black;">
        Your browser doesn't support the canvas tag.
    </canvas>
</div>

I needed a named div to access programmatically, so I created one called “panel”.  I also changed the styling on the canvas so the background color would be black, making debugging a bit easier.  The dimensions passed to the canvas are going to be completely ignored.  Why the heck Canvas didn’t support % layout, I will never understand.

 

Finally, the majority of changes are in map.View.js, which I basically re-wrote:

YUI.add('mapView',function(Y){
    var Instance = null;
    Y.MapView = Y.Base.create('mapView', Y.View, [], {
        events:{
          "#mainCanvas": {
              click:function(e)
              {
                  alert("Blah");
              }
          }
        },
        initializer:function(){
            Instance = this;
            var results = Y.io('/scripts/views/templates/map.Template',{"sync":true});
            template = Y.Handlebars.compile(results.responseText);
        },
        prepareCanvas:function(){
            this.resizeEvent();
            createjs.Ticker.setFPS(30);
            createjs.Ticker.addListener(this.gameloop);

            Y.on('windowresize',this.resizeEvent);
        },
        render:function(){
            if(this.template === null)
                this.initializer();
            this.get('container').setHTML(template());
            this.prepareCanvas();
            return this;
        },
        gameloop:function(){
            Instance.stage.update();
            Instance.stage.getChildAt(0).x++;
            if(Instance.stage.getChildAt(0).x > Instance.stage.canvas.width)
                Instance.stage.getChildAt(0).x = 0;
        },
        resizeEvent:function(){
            var container = Instance.get('container');
            var canvas = container.one("#mainCanvas");
            var panel = container.one('#panel');

            var body = Y.one("body");
            var screenWidth = body.get("clientWidth");
            var screenHeight = body.get("scrollHeight");

            var width = Math.floor(screenWidth -280);
            var height = Math.floor(screenHeight );

            canvas.setStyle("width",width);
            canvas.setStyle("height",height);

            this.stage = new createjs.Stage(canvas.getDOMNode());
            // for some reason, easel doesn't pick up our updated canvas size so set it manually
            this.stage.canvas.width = width;
            this.stage.canvas.height = height;

            var shape1 = new createjs.Shape();
            shape1.graphics.beginFill(createjs.Graphics.getRGB(0,255,0));
            shape1.graphics.drawCircle(200,200,200);

            this.stage.addChild(shape1);
        }
    });
}, '0.0.1', { requires: ['view','event','io-base','handlebars']});

 

The variable Instance is a horrible hack, that I intend to replace at some point in the future.  That’s the joy of exercises like this, I can refactor out my hacks later on.  So, why does it exist… well you see, I make a couple of functions that are called back by external code, which completely clobber my this pointer.  I suppose it’s a poor mans singleton of sorts.

 

The end result of this code:

 

 

First thing I did within MapView is declare an event if someone clicks on our canvas ( which needs the id mainCanvas… this is another hackish solution that should possibly be factored away, although frankly, I am OK with requiring the canvas tag to have a certain ID, so I probably wont ) this function is called.  It was simply written to figure out how YUI views handled events.  All it does is pops up an alert with the text Blah.  As you can see, handling element level events in relatively simple, although sadly I couldn’t figure out how to capture document level events here.  Another thing on the todo list.

 

In the initializer function I take a copy of the this pointer in the Instance variable ( *hack* *hack* ), and have changed the template fetching code to no longer be asynchronous, to completely remove some unnecessary race conditions that can result from a network delay retrieving the template. Frankly in this case, async bought us nothing but headaches.

 

prepareCanvas is the method responsible for setting up the easelJS integration.  It starts off by calling resizeEvents, which is where the bulk of the actual work is done.  resizeEvents was factored out to a separate function, because this logic is the same on initial creation as it is when the window is resized.  When resizeEvent() is called, we first find the BODY tag, and get its width and height using clientWidth and scrollHeight.  You would think the obvious value would be clientHeight, but you would be wrong, this is just one of those ways that HTML sucks.  Once we have the width and height, we then calculate our view dimensions, by subtracting the space needed for the other views ( or… will soon for height that is ).  We then set the canvas to those dimensions using setStyle(), which resizes the CANVAS in the browser.  We then create our Stage object from our canvas.  One thing to keep in mind, YUI get() and one() functions return YUI Node objects, not actual DOM objects, so when dealing with 3rd party libraries, you need to access the actual DOM item the node contains, that can be done with .getDOMNode().  Next we manually update the stage.canvas width and height, because of what I can only assume is a bug, EaselJS doesn’t pick up the modifications we made to the Canvas dimenions… who knows, there might be something else going on behind the scenes.  Next we create a circle… just so we have something visible on screen, and add it to our stage.

 

Now that resizeEvents is done, back in prepareCanvas we then set up a Ticker, which is an EaselJS callback mechanism, somewhat like setTimeout.  This is the heartbeat of your application, and due to the setFPS(30) call, it *should* be called 30 times per second.  This is your traditional game loop within the application and will probably be used quite a bit in the future.  Finally we handle windowresize events using the Y.on() handling mechanism, to catch the case the user resizes the screen, and if they do we call resizeEvents ( which being an eventhandler, will clobber all over our this pointer ).

 

Finally, we have the aforementioned gameloop, which is a function that is going to be called every time createjs.Ticker, um… Ticks.  For now we simply update our stage, then find the one and only item on our stage with getChildAt(0), which is our circle, and increment it’s X value until it scrolls off the screen.

 

It seems a bit more complicated than it is, but the basics of most of what we are going to want to deal with is now in place.  We can handle UI events via YUI, can render to the Canvas using the EaselJS library and best of all, take full advantage of the screen resolution, no matter how big or small, and gracefully handle changes in size, something Canvas doesn’t do easily.

 

Figuring out how everything interacted was more of a headache than I expected, but I am reasonably happy with this setup for now.  Of coure, I am going to need to add real functionality to the map view, and have it feed by a Map model instead of just drawing a circle on screen, but all things in time.

 

You can download the complete project as of this point right here.

You can see the project in action by clicking here.  As you resize, so will the canvas element.

Programming, Design , , ,

Month List

Popular Comments

JavaScript Toddler Game Part 3: App configuration
Subscribe to GameFromScratch on YouTube Support GameFromScratch on Patreon


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 , , , ,

blog comments powered by Disqus

Month List

Popular Comments