Subscribe to GameFromScratch on YouTube Support GameFromScratch on Patreon
3. June 2013

 

As I am just starting out on a new game these days, I am in the most wishy-washy stage of development, design.  Over the years, the way I went about designing an application has changed greatly as technology, my team size and frankly, me, have changed.  Now that I work mostly alone I find I am a lot less formal than I used to be.

 

In the ancient days, I ignored design completely, or used a spiral bound binder and a pencil.  On my last large scale project I worked mostly in Visio for object design and program flow modelling.  For idea capture and design documents, OneNote was my loyal ally.  If you find yourself working using a paper and pencil most often, you should check out OneNote, it is probably the greatest application Microsoft make and I say that without hyperbole.  It is an under-exposed and under-appreciated application.  Whenever I see websites comparing Evernote or Google Keep I laugh and laugh and laugh.  Those two programs aren't in the same league as OneNote.  That said, the OneNote mobile offerings, at least on iOS and Android are pretty terrible and the web version is only ok.  The application version though, its just great. 

 

That said, this combination of applications has definite limits.  These days I am working more commonly across platforms, splitting my time about 50/50 between MacOS and Windows.  Unfortunately Mac Office doesn't include OneNote and Visio isn't available either.  Also, I am no longer part of MSDN and Visio is stupidly expensive.  The other major change is the rise of tablets.  I find myself on mobile much more often now, either the iPad or my Galaxy Note ( which you can pry from my cold dead hands! ).  So, if I am out and about and an idea comes to me, its nice to just whip out my phone or tablet, which are on instantly, jot the idea down and move on.  Additionally, I find touch a much nicer interface when working on a fluid design.

 

One of the flaws of working with code modelling software is it is often too advanced for me.  I don't need it to validate my class design, I am not working in a team or with contractors, so I don't need it to create formal design documents or strictly defined interfaces.  At the end of the day I generally just like to enter a class name and description, then model relationships between other classes.  That or create flow charts that illustrate user or application flow.  Visio can do this, but it is massive overkill.  Enter the mind map.

 

In my life, I always had a bit of a mental block towards this kind of software…  I don't really know why, but I never really looked into it closer and I regret that, as mind mapping is basically what I've needed all along.  Essentially mind mapping is just a diagramming software that models relationships.  That's about it and exactly what I am looking for.  There are reams of mind mapping applications out there, from downloadable applications to web services, from free to very expensive.  If you want to try one out free, you can try FreeMind or XMind to get started.  If you haven't tried one yet, you really should.

 

Myself, I am using a combination of applications for program design.  My design documents and "raw text dump" application of choice is still OneNote.  I vastly prefer authoring in the full application in Windows, but the web application and mobile apps work in a pinch, at least for reading.  On iPad I use iThoughtsHD, which you can see in action below:

IThoughtsHD

I haven't found a mind map application that works well on my Note, but I've also barely looked.  I like iThoughtsHD because it's quick, intuitive and supports the functionality I need. On top, it is compatible with most other Mindmap applications including FreeMind and Xmind that I mentioned earlier.  So I can design on the go, then open on my desktop on any platform.  Truth is though, I vastly prefer working on the tablet, it is just such an intuitive format for this kind of work.

 

If there is any interest, I will go into a bit more detail on Mind mapping and iThoughtsHD for game design.  I know this is a topic that isn't generally discussed very often, although that could be because people find it boring! ;)

 

So, that's what I use these days to design my applications ( and other projects ).  What do you use?

Design


6. November 2012

 

Windows Phone 7 was a pretty capable platform that never really took off, especially when it came to game support.  Of course, market share is a big part of the reason was the lack of applications ( although it is easy to get in a chicken and egg argument, whether market share suffered because of the lack of app support, which suffered because of the lack of market share… ).

 

With Windows Phone 8 however, Microsoft have rectified at least one of these problems.  You can now compile native C++ code.  This means a gigantic collection of existing libraries can be ported to or now target the platform.

 

And ported they have been, here is the list to date.  Keep in mind, pretty much every single project that runs on Windows 7 will run on Windows 8 Desktop, so “Windows 8 support” is a pretty meaningless statement.  The following are all products that will support Windows RT, the artist previously known as Metro.  Or in more simple terms, you can use the following to make games for ARM powered Windows 8 devices.

If I have missed any, please let me know!

 

cocos2d-x

http://www.cocos2d-x.org/news/76

Construct2

https://www.scirra.com/blog/99/make-windows-8-games-with-construct-2

GameMaker

http://www.yoyogames.com/gamemaker/studio/multiformat/windows8

GameSalad

http://gamesalad.com/creator/windows-8

Havok

(All products) http://www.havok.com/news-and-press/releases/havok-showcase-full-technology-suite-windows-8-windows-rt-and-windows-phone

Marmalade

http://www.madewithmarmalade.com/press/cross-platform-games-apps-sdk-marmalade-announces-support-microsoft-windows-phone-8-q4-2012

MonoGame

http://monogame.codeplex.com/

Ogre3D

http://www.ogre3d.org/2012/10/30/ogre-now-supports-windows-phone-8

SharpDX

http://sharpdx.org/news/new-version-2-4-0

Shiva3D

(road map only at this point)

http://www.shivaengine.com/developer/2290-the-shiva-2012-13-roadmap

Unigine

http://unigine.com/devlog/2012/07/20/100

Unity

http://blogs.unity3d.com/2012/10/30/unity-windows-phone-8-demonstrated-at-microsoft-build-conference/

Unreal/UDK

http://blogs.nvidia.com/2012/08/nvidia-brings-unreal-engine-3-to-windows-8-and-windows-rt/

 

 

Somewhat related or slightly less game oriented products that added Windows 8 support

 

Appcelerator Titanium

(Q1 2013)

http://thinkmobile.appcelerator.com/press-releases/bid/236017/Appcelerator-Empowers-the-Mobile-Developer-Community-With-Even-More-Choice-and-Flexibility

appMobi

http://www.appmobi.com/?page_id=453

PhoneGap/Apache Cordova

http://phonegap.com/blog/2012/10/30/announcing-apache-cordova-support-for-windows-phone-8/

Design Programming


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="https://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


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="https://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


GFS On YouTube

See More Tutorials on DevGa.me!

Month List