Creating game authoring tools in HTML5 table of contents created

19. February 2013

I have been working on a long running, but slow in development, series of posts on authoring a level creation tool using HTML5.  It covers how to actually create an application using the popular MVC design pattern, implemented using the YUI3 libraries, as well as the EaselJS graphic library.

 

If you are interested in HTML5 application development, or in a ( simple for now ) level editor, hopefully this series is of interest to you.  

 

 

Table of contents link.

 

 

Current Contents:

 

Expect this to be updated over time as I continue.

Programming , ,




Creating game creation tools using HTML5: Redux

26. October 2012

 

Due to a bunch of great feedback I received from the YUI community and learning a bit more about how YUI works, I’ve made some minor, but extensive ( yes, that actually makes sense ) changes to the guts of my upcoming HTML based level editor.

 

As a bit of a recap, so far we have covered:

Creating the basic MVC framework

Integrating the EaselJS canvas library

Adding an application menu (that does nothing)

Adding a file upload dialog

 

In this section, we are going to simply clean things up a bit.  Add a layer of polish, remove some of the hackish behaviour and simply make it a better foundation.  Instead of simply editing the previous posts, I figured there was some value in seeing the evolution of an application. In some ways, nothing illustrates a concept better than a before and after.

 

this = that = gross;

 

A quirk of JavaScript is that it absolutely clobbers the this pointer in callbacks.  Of course, it’s all a matter of perspective if this is a feature or not, but from someone who is from a C++/Java/C# background it certainly seems alien, you certainly wouldn’t expect the value of this to change within the same code file, but of course it does.  A very common work around is to copy this into another variable, often that or self at a higher scope, but there are certainly limitations ( plus it feels like a hack ).  Consider this common simplified example:

var that=this;
buttonDone.on("click", function(){
    that.doSomething();
})

In most (all?) YUI handlers you are actually able to solve this with incredible ease. You can pass the context in as a parameter:

buttonDone.on("click", function(){
    this.doSomething();
},this)

This is a change I made through-out the project.  However, what happens when you are dealing with a non-YUI method?  A very good example is in map.View.js, we provide a callback function that the EaselJS library calls each frame.  How exactly do we deal with that?  Consider:

createjs.Ticker.addListener(this.gameloop);

How do you handle the this value getting clobbered in this situation?  I used a global variable named Instance, which obviously was a gross hack.  I sadly couldn’t extended the callback to accept a context without making massive changes to the easelJS library, which obviously I don’t want to do.  So, how then do you cleanly solve this issue?  With incredible ease apparently:

createjs.Ticker.addListener(Y.bind(this.gameloop,this));

That’s it…  just wrap your function parameter in a Y.bind() call, and pass in the context you wish to be bound and VOILA, this is preserved.  How does it work?  ….  Black magic probably, with a great deal of chickens being sacrificed.

 

These two changes, passing the context when possible or using Y.bind() when not, reduced a great many horrible hacks from the code and made me feel a great deal better about life, the universe, everything…

 

If you support templates to make life easier for designers, why the hell aren’t you using style sheets?

 

That’s a very good question to which I simply do not have a good answer.  When I did most of my development work in HTML, it was a world without CSS and it is a technology I never really took to.  In a world where CSS selectors are increasingly important, and in an application I am making designer friendly, that is not a valid excuse. 

 

Therefore, I pulled most of the styling out to a style sheet.  This also means I removed various JavaScript based styling calls.  I also added the YUI style skin yui-skin-sam the to app <BODY> tag in index.html.  This was missed mostly out of … well, I kinda forgot I had a body tag.  Part of my brain thought that editor.View.js was the root level HTML construct, I completely forgot about the contents of index.html.

 

In order to add stylesheet support, I added a root level directory called stylesheets and created the file style.css within.  It also required adding an additional route for express in server.js, in case you are hosting from node.

server.use('/stylesheets', express.static(__dirname + '/stylesheets'));

This line basically just adds another directory to serve static files from.  If you didn’t add this, you will get 404 errors when you request a stylesheet.

 

Speaking of templates…

 

Copy and paste coding rather bit me in the butt here.  You see, I started from the person.View.js and person.js as a starting point, code that was never intended to be in the final product and code that contained a great deal more problems then I realized.  Code however, that also demonstrated the complete lifecycle of populating a view with a model, and compiling and displaying a template.

Problem is, thus far in this application, we have NO DATABINDING.  None.  It will of course come later, but most templates are actually just straight HTML with no need to process.  Thing is, I was compiling them anyways, like so:

var results = Y.io('/scripts/views/templates/map.Template',{"sync":true});
template = Y.Handlebars.compile(results.responseText);

Which was a waste of processing power. So instead we simply do:

var results = Y.io('/scripts/views/templates/map.Template',{"sync":true});
template = results.responseText;

There is the possibility that templates are overkill and handlebars is too heavy weight, and this is quite likely true.  At the end of the day though, this isn’t an application that needs to scale out massively, so I don’t really need to squeeze every cycle, so I will stick with handlebars templates for now.  The nice thing about templates is, they can be swapped out relatively easily later on.  Lightweight or not, handlebars is one of the most popular templating engines.

 

To async or not to async

 

One other areas of feedback I got, that I am not sure I entirely agree with, is that I should be loading the templates asynchronously. On the surface, this certainly makes sense, as JavaScript is a highly asynchronous language ( taken to laughable extremes at times… you will know what I mean if you’ve worked in Node.js and found yourself nested 5 or 6 callbacks deep ) and the DOM certainly encourages an async model.  Your UI will “hang” while waiting on code to complete unless it is handled asynchronously.

My catch is, this is exactly what *should happen*.  Loading a template is a synchronous task, period.  All of the rest of your code is going to be spent first checking to see if the template has loaded before proceeding.  Nothing can happen until the template has loaded, period.  Therefore it makes little sense to perform a serial action in parallel.

That said, this is just *my* opinion on the matter.  I was however offered an elegant solution to the complexity of dealing with async callbacks, and I figured I would share it here.  So here is the person.View.js rewritten to work async:

YUI.add('personView',function(Y){
        Y.PersonView = Y.Base.create('personView', Y.View, [], {
        initializer:function(){
            this.pending = new Y.Parallel();
            Y.io('/scripts/views/templates/person.Template',{
                on:{
                    complete:this.pending.add(function(id,response){
                        template = Y.Handlebars.compile(response.responseText);
                    })
                }
            },this);
        },
        render:function(){
            this.pending.done(Y.bind(function(){
                this.get('container').setHTML(template(this.get('model').getAttrs()));
            },this));

            return this;
        }
    });
}, '0.0.1', { requires: ['view','io-base','person','handlebars','parallel']});

 

The secret sauce here is the Y.Parallel module.  It allows you to batch up a number of parallel functions, which provides a callback for when they are all complete.  If you are following along and prefer to go pure async, use the above code as a template, or better yet, refactor to a common base class shared between your views.

 

A little longer, a lot less ugly

 

One other thing I hated about the previous code was the <SCRIPT> mess of includes that was developing at the top of index.html.  As of the last update, it looked like:

<script src="http://yui.yahooapis.com/3.5.1/build/yui/yui-min.js"></script>
<script src="http://code.createjs.com/easeljs-0.5.0.min.js"></script>
<script src="/scripts/models/person.js"></script>
<script src="/scripts/models/spriteSheet.js"></script>
<script src="/scripts/views/person.View.js"></script>
<script src="/scripts/views/map.View.js"></script>
<script src="/scripts/views/mainMenu.View.js"></script>
<script src="/scripts/classes/AddSpriteSheetDialog.js"></script>
<script src="/scripts/views/editor.View.js"></script>

 

This is ugly and only going to get uglier and I knew there had to be a better way, I just didn’t know what it was.  I thought the Y.Loader was a likely candidate, but I was wrong ( but very close ).  Instead there is a global variable called YUI_config you can use to declare all of your custom modules and their dependencies.  Therefore I created a new file named /scripts/config.js with the following contents:

YUI_config = {
    groups: {
        classes: {
            base: 'scripts/classes',
            modules:{
                addSpriteSheetDialog: {
                    path:'/addSpriteSheetDialog.js',
                    requires: ['node','spriteSheet','panel']
                }
            }
        },
        models: {
            base: 'scripts/models',
            modules: {
                person: {
                    path: '/person.js',
                    requires: ['model']
                },
                spriteSheet: {
                    path: '/spriteSheet.js',
                    requires: ['model']
                },
                tile: {
                    path: '/tile.js',
                    requires: ['model']
                }
            }
        },
        views: {
            base: 'scripts/views',
            modules: {
                editorView: {
                    path: '/editor.View.js',
                    requires: ['view','io-base','addSpriteSheetDialog','personView',
                        'mainMenuView','mapView','event-custom','handlebars']
                },
                mainMenuView: {
                    path: '/mainMenu.View.js',
                    requires: ['view','io-base','node-menunav','event','handlebars']
                },
                mapView: {
                    path: '/map.View.js',
                    requires: ['view','event','io-base','handlebars']
                },
                personView: {
                    path: '/person.View.js',
                    requires: ['view','io-base','person','handlebars']
                }
            }
        }
    }
}

 

This allows the YUI loader to load your scripts and their dependencies.  Ideally too, this allows the loader to load them asynchronously, which in this case is a very good thing.  Ideally then, this will cause your app to load quicker.

 

Y.App, I hardly knew you!

 

On other thing that has been mentioned ( a couple times from a couple sources ) is I am not really making use of Y.app routing, and this is 100% true, I am not.  As you can see in index.html:

    YUI().use('app','editorView', function (Y) {

        var app = new Y.App({
            views: {
                editorView: {type: 'EditorView'}
            }
        });

        app.route('*', function () {
            this.showView('editorView');
        });

        app.render().dispatch();
    });

So, yeah, a router with exactly one route is rather pointless.  So, why do I have it at all?

Well, that’s mostly a matter of reality not matching expectations and is a bi-product of “winging it”.  As things developed, once I chose to go with a composite view, the parent view editor.View.js essentially usurped the roll of controller from Y.app, which is perfectly OK.

So, why keep Y.App?  Well it’s perfectly possible that I will have tasks outside of the single composite view, in which case the app will be used.  If not, it is easily used later.  If you were looking at the code and thinking “hmmmm… that code seems superfluous”, you were exactly right.

 

Summary

 

Almost every “code smell” I had is now gone, which always makes me feel better about things. The experience also enlightened me to some of the nuances of YUI.  A great deal of thanks to Satyam on the YUI forums for taking the time to educate me.  My thanks again to all others who have commented or messaged me.  Now back to adding new features!

 

The Code

 

You can download the new sources right here.

 

As pretty much every single file changed, I am just going to dump full sources below.

 

At this point in time, our project looks like:

image

 

index.html

<!DOCTYPE html>

<html>
<head>
    <title>GameFromScratch example YUI Framework/NodeJS application</title>
</head>
<body class="yui3-skin-sam">


<script src="http://yui.yahooapis.com/3.5.1/build/yui/yui-min.js"></script>
<script src="http://code.createjs.com/easeljs-0.5.0.min.js"></script>
<script src="scripts/config.js"></script>
<link rel="Stylesheet" href="/stylesheets/style.css" />

<script>
    YUI().use('app','editorView', function (Y) {

        var app = new Y.App({
            views: {
                editorView: {type: 'EditorView'}
            }
        });

        app.route('*', function () {
            this.showView('editorView');
        });

        app.render().dispatch();
    });
</script>


</body>
</html>

server.js

var express = require('express'),
    server = express();

server.use('/scripts', express.static(__dirname + '/scripts'));
server.use('/stylesheets', express.static(__dirname + '/stylesheets'));

server.get('/', function (req, res) {
    res.set('Access-Control-Allow-Origin','*').sendfile('index.html');
});

server.listen(process.env.PORT || 3000);

 

config.js

YUI_config = {
    groups: {
        classes: {
            base: 'scripts/classes',
            modules:{
                addSpriteSheetDialog: {
                    path:'/addSpriteSheetDialog.js',
                    requires: ['node','spriteSheet','panel']
                }
            }
        },
        models: {
            base: 'scripts/models',
            modules: {
                person: {
                    path: '/person.js',
                    requires: ['model']
                },
                spriteSheet: {
                    path: '/spriteSheet.js',
                    requires: ['model']
                },
                tile: {
                    path: '/tile.js',
                    requires: ['model']
                }
            }
        },
        views: {
            base: 'scripts/views',
            modules: {
                editorView: {
                    path: '/editor.View.js',
                    requires: ['view','io-base','addSpriteSheetDialog','personView',
                        'mainMenuView','mapView','event-custom','handlebars']
                },
                mainMenuView: {
                    path: '/mainMenu.View.js',
                    requires: ['view','io-base','node-menunav','event','handlebars']
                },
                mapView: {
                    path: '/map.View.js',
                    requires: ['view','event','io-base','handlebars']
                },
                personView: {
                    path: '/person.View.js',
                    requires: ['view','io-base','person','handlebars']
                }
            }
        }
    }
}

 

style.css

body { margin:0px;overflow:hidden; }

#mapPanel { margin:0px;float:left;display:block; }

#mapPanel #mainCanvas { background-color:black; }

.spritesheetDialog { spadding-top:25px;padding-bottom:25px; }

person.js

YUI.add('person',function(Y){
    Y.Person = Y.Base.create('person', Y.Model, [],{
            getName:function(){
                return this.get('name');
            }
        },{
            ATTRS:{
                name: {
                    value: 'Mike'
                },
                height: {
                    value: 6
                },
                age: {
                    value:35
                }
            }
        }
    );
}, '0.0.1', { requires: ['model']});

 

spriteSheet.js

YUI.add('spriteSheet',function(Y){
    Y.SpriteSheet = Y.Base.create('spriteSheet', Y.Model, [],{
            count:function(){
                return this.get('spritesheets').length;
            },
            add:function(name,width,height,img){
                this.get('spritesheets').push({name:name,width:width,height:height,img:img});
            }
        },{
            ATTRS:{
                spritesheets: {
                    value: []
                }
            }
        }
    );
}, '0.0.1', { requires: ['model']});

 

tile.js (ok, this one is new… )

YUI.add('tileModel',function(Y){
    Y.Person = Y.Base.create('tile', Y.Model, [],{
            getName:function(){
                return this.get('name');
            }
        },{
            ATTRS:{
                src: {
                    value: ''
                },
                offsetX: {
                    value: 0
                },
                offsetY: {
                    value:0
                },
                width: {
                    value:0
                },
                height:{
                    value:0
                }

            }
        }
    );
}, '0.0.1', { requires: ['model']});

 

editor.View.js

YUI.add('editorView',function(Y){
    Y.EditorView = Y.Base.create('editorView', Y.View, [], {
        spriteSheets:new Y.SpriteSheet(),
        initializer:function(){

            var person = new Y.Person();
            this.pv = new Y.PersonView({model:person});
            this.menu = new Y.MainMenuView();
            this.map = new Y.MapView();

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

            Y.Global.on('menu:fileAddSpriteSheet',function(e){
                var dialog = Y.AddSpriteSheetDialog.show(this.spriteSheets, Y.bind(function(){
                    var sheet = this.spriteSheets.get("spritesheets")[0];
                    console.log(sheet);
                },this));
            },this);
        },
        render:function(){
            var content = Y.one(Y.config.doc.createDocumentFragment());
            content.append(this.menu.render().get('container'));

            var newDiv = Y.Node.create("<div style='width:100%;margin:0px;padding:0px'/>");
            newDiv.append(this.map.render().get('container'));
            newDiv.append(this.pv.render().get('container'));

            content.append(newDiv);
            this.get('container').setHTML(content);
            return this;
        }
    });
}, '0.0.1', { requires: ['view','io-base','addSpriteSheetDialog','personView',
    'mainMenuView','mapView','event-custom','handlebars']});

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){
                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']});

map.View.js

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

            Y.on('windowresize',this.resizeEvent,this);
            this.publish('windowresize');
        },
        render:function(){
            this.get('container').setHTML(template);
            this.prepareCanvas();
            return this;
        },
        gameloop:function(){
            this.stage.update();
            this.stage.getChildAt(0).x++;
            if(this.stage.getChildAt(0).x > this.stage.canvas.width)
                this.stage.getChildAt(0).x = 0;
        },
        resizeEvent:function(){
            var container = this.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 + "px");
            canvas.setStyle("height",height + "px");

            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']});

person.View.js (async version)

YUI.add('personView',function(Y){
        Y.PersonView = Y.Base.create('personView', Y.View, [], {
        initializer:function(){
            this.pending = new Y.Parallel();
            Y.io('/scripts/views/templates/person.Template',{
                on:{
                    complete:this.pending.add(function(id,response){
                        template = Y.Handlebars.compile(response.responseText);
                    })
                }
            },this);
        },
        render:function(){
            this.pending.done(Y.bind(function(){
                this.get('container').setHTML(template(this.get('model').getAttrs()));
            },this));

            return this;
        }
    });
}, '0.0.1', { requires: ['view','io-base','person','handlebars','parallel']});

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="menuFileAddSpriteSheet">
                        <a class="yui3-menuitem-content" href="#">Add SpriteSheet</a>
                    </li>
                    <li class="yui3-menuitem" id="menuFileExit">
                        <a class="yui3-menuitem-content" href="#">Exit</a>
                    </li>
                </ul>
                    </div>
                </div>
                </li>
            </ul>
        </div>
    </div>
</div>

map.Template

<div id="mapPanel">
    <canvas width=300 height=300 id="mainCanvas" >
        Your browser doesn't support the canvas tag.
    </canvas>
</div>

person.Template

<div style="width:250px;min-width:250px;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> 

** – person isn’t styled because this is a place holder view anyways and is going to be removed from the project once I have an actual demonstration of a data-bound template.

Again, the entire archive can be downloaded here.

Programming, Design ,




Creating game creation tools using HTML5: Adding a sprite sheet upload dialog

17. October 2012

 

A level is made up of sprites and sprites come from somewhere.  In our editor, we are going to allow the user to “upload” multiple image files containing sprite sheets.  However, are server is not required and that is going to require a bit of work.  Also, we are going to need some form of UI where users can upload the spritesheet, without cluttering our main UI too much, so we will implement it as a modal dialog box.

 

Well, let’s get to it.  First lets create a data type for holding our sprite sheet collection.  For now, a spritesheet is simply an image, the dimensions of each sprite and a name.  In your models folder create a new file named spriteSheet.js

spriteSheet.js

 

YUI.add('spriteSheet',function(Y){
    Y.SpriteSheet = Y.Base.create('spriteSheet', Y.Model, [],{
            count:function(){
                return this.get('spritesheets').length;
            },
            add:function(name,width,height,img){
                this.get('spritesheets').push({name:name,width:width,height:height,img:img});
            }
        },{
            ATTRS:{
                spritesheets: {
                    value: []
                }
            }
        }
    );
}, '0.0.1', { requires: ['model']});

Nothing really special.  Our spritesheets attribute is just an empty array for now.  We also included a pair of methods, add, for adding a new spritesheet and count for getting the current count of spritesheets already declared.  Everything else here should already be familiar at this point.

 

Now we want to create a dialog that will be displayed when the user wants to add a spritesheet.  As a bit of a spoiler, here is what we are going to create:

image

This isn’t a View and it isn’t a model, so we create a new folder called classess and create the long-winded file named AddSpriteSheetDialog.js

AddSpriteSheetDialog.js

YUI.add('addSpriteSheetDialog', function(Y){

    Y.AddSpriteSheetDialog = new Y.Base();
    var spriteSheets = null;
    Y.AddSpriteSheetDialog.show = function(ss,onComplete){
        spriteSheets = ss;
        var panel = new Y.Panel({
            width:500,
            height:300,
            centered:true,
            visible:true,
            modal:true,
            headerContent:'Select the image file containing your sprite sheet',
            bodyContent:Y.Node.create(
                "<DIV>\
                <input type=file id=spritesheet /> \
                <br /> <div id=imgName style='padding-top:25px;padding-bottom:25px'> \
                Click above to select a file to download</div>\
                <br />Sheet name:<input type=Text id=name size=30 value=''> \
                <br />Sprite Width:<input type=Text id=width size=4 value=32> \
                Sprite Height:<input type=Text id=height size=4 value=32> \
                <br /><input type=button id=done value=done />\
                </DIV>\
                "
            ),
            render:true
        });

        var fileUpload = Y.one("#spritesheet");
        fileUpload.on("change", Y.AddSpriteSheetDialog._fileUploaded);

        var buttonDone = Y.one("#done");
        buttonDone.on("click", function(){
            panel.hide();
            onComplete();
        })
        panel.show();

    };

    Y.AddSpriteSheetDialog._fileUploaded = function(e){
        if(!e.target._node.files[0].type.match(/image.*/)){
            alert("NOT AN IMAGE!");
            return;
        }
        var selectedFile = e.target._node.files[0];
        var fileReader = new FileReader();

        var that=this;
        fileReader.onload = (function(file){
            return function(e){
                if(e.target.readyState == 2)
                {
                    var imgData = e.target.result;
                    var img = new Image();
                    img.onload = function(){
                        Y.one('#imgName').set('innerHTML',selectedFile.name + " selected");
                        var name = Y.one('#name').get('value');
                        var width = Y.one('#width').get('value');
                        var height = Y.one('#height').get('value');
                        spriteSheets.add(name,width,height,img);
                    }
                    img.src = imgData;
                }
            };

        })(selectedFile);
        fileReader.readAsDataURL(selectedFile);

    };


},'0.0.1', {requires:['node','spriteSheet','panel']});

The editorView owns the spritesheet collection, and passes it in to the show() method of AddSpriteSheetDialog.  We also pass in a callback function that will be called when we are done.

We start off creating the panel which is a Y.Panel.  Most of the properties should be pretty straight forward, headerContent is the title and bodyContent is either the ID of the object to render the panel in, or in our case, we actually create a new node with our dialog HTML.  We then wire up a change handler on our file upload button, this will fire when a file is uploaded and call the _fileUploaded function.  We then wire up the Done button’s on click handler to hide the panel then call the callback function that was passed in.  Finally we display the panel.

 

When the user clicks the Choose File button, _fileUploaded is called.  First thing we check to make sure it is an image that is uploaded and error out if it isn’t.  We then want to read the selected file, which we do with the FileReader api.  Word of warning, this isn’t completely supported in every browser… frankly though, I don’t care about supporting IE in a project like this, cross browser support takes all of the fun out of web app development! Smile

 

Next is well… JavaScript at it’s most confusing. We are registering an onload event that will be fired once the file has been loaded, which in turn fires off an anonymous method.  It checks the readystate of the file to make sure it is ready and if so, our “uploaded” file will be in e.target.result.  We then create an Image object, then register yet another onload handler, this one for when the image has completed loading.  Once the user has uploaded the file, its finished loading and populated in our newly create Image, we then get the width, height name and our newly populated image and at it to the screenSheets object we passed in during show().  Yes, this is a bit screwy of an interface, in that you need to populate the text fields before uploading the interview.  I will ultimately clean that up ( and add edit ability ), but it would needlessly complicate the code for now.  Finally, no that our fileReader.onload() event is done, we actually read the file now with readAsDataUrl() the file that was chosen, which fires off the whole onload event handler in the first place.   Welcome to asynchronous JavaScript programming!  Don’t worry, if this is new to you, thinking async will come naturally soon enough…

 

So, that is how you can create a modal dialog to edit app data.  Now we wire it up and deal with a bit of a gotcha.

 

The gotcha first…  the Panel dialog requires a parent HTML element in the DOM to have a YUI skin CSS class declared.  At the bottom on the render function in editor.View.js add the following code:

Y.one('body').setStyle("margin",0);
Y.one('body').setStyle("overflow","hidden");
// The below needs to be added as some controls, such as our add sprite dialog, require a parent container
// to have the YUI skin defined already
Y.one('body').setAttribute("class","yui3-skin-sam");
return this;

This adds the yui3-skin-sam class to the page’s body, which brings in all the styling for the Panel ( and other YUI widgets ).

 

While we are in editor.View.js, we wire up a menu handler for when the user clicks the add spritesheet button ( we will add in a second ).  That handler is basically the same as the menu:fileExit handler we created earlier.  Right below that handler in the initializer function, add the following:

 

var that = this;
Y.Global.on('menu:fileAddSpriteSheet',function(e){
    var dialog = Y.AddSpriteSheetDialog.show(that.spriteSheets,function(){
        var sheet = that.spriteSheets.get("spritesheets")[0];
        console.log(sheet);
    });
});

There is the that=this hack again, there are alternatives ( you can pass the context in to the Y.Global.on event handler ), but this is a fair bit easier at the end of the day, as we would lose this again when the callback is called.  Otherwise, when the menu:fileAddSpriteSheet event is received, we simply call AddSpriteSheetDialog.show(), passing in our spritesheet and the function that is called when the panel is complete.  For now we simply log the spritesheet out to the console to prove something changed.

We also need to add the SpriteSheet to our editor.View.js, like so:

 

 Y.EditorView = Y.Base.create('editorView', Y.View, [], {
        spriteSheets:new Y.SpriteSheet(),
        initializer:function(){

 

Now we need to add the menu item.  First add it to the template mainMenu.Template,like so:

<ul>
    <li class="yui3-menuitem" id="menuFileAddSpriteSheet">
        <a class="yui3-menuitem-content" href="#">Add SpriteSheet</a>
    </li>
    <li class="yui3-menuitem" id="menuFileExit">
        <a class="yui3-menuitem-content" href="#">Exit</a>
    </li>
</ul

And we wire it up in the mainMenu.View.js, add the bottom of render() add the following code:

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

Oh, and our newly added script AddSpriteSheetDialog.js is added to index.html to guarantee it gets loaded and evaluated.

 

And done.  We now added a dialog for adding sprite sheet images, and can store the image results locally without requiring any server interaction at all.

 

Here is the end result, select File->Add Spritesheet to bring up the newly created dialog:

 


You can download the entire updated source code here.

One step closer to a full web based game editor, one very tiny step. Smile

Programming, General , , ,




Creating game creation tools using HTML5: Let there be menus!

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




Creating game creation tools using HTML5: Setting up the EaselJS canvas

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