Subscribe to GameFromScratch on YouTube Support GameFromScratch on Patreon


21. December 2012

So my HTML5 game tool creation tutorial series took a bit of a hiatus, but now it's back.  As a quick recap, if you are interested in creating a) game tools using HTML5/JavaScript and/or b) are looking for examples of non-trivial web applications, this is the series for you!  Keep in mind though, I am not spending a lot of forethought on the whole process, so the design may not be the most sound!

 

 

In this section, we are doing very little new, it's mostly all stuff we covered in the past, just with an eye towards adding more features to our application.

 

First we make a small convenience change to our code.  Previously the workflow in our add sprite sheet dialog addSpriteSheetDialog.js was a bit… ugly.  The order you filled in the dialog were important and arbitrary.  It made no sense to have to upload the file before you could fill in the fields, so we sorted that with a fairly minor series of changes.

 

addSpriteSheetDialog.js

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

 

    Y.AddSpriteSheetDialog = new Y.Base();

    var spriteSheets = null;

    var name,width,height,img = 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 class='spritesheetDialog'> \

                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(){

            name = Y.one('#name').get('value');

            width = Y.one('#width').get('value');

            height = Y.one('#height').get('value');

            spriteSheets.add(name,width,height,img);

            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();

 

        fileReader.onload = (function(file){

            return function(e){

                if(e.target.readyState == 2)

                {

                    var imgData = e.target.result;

                    img = new Image();

                    img.onload = function(){

                        Y.one('#imgName').set('innerHTML',selectedFile.name + " selected");

                    }

                    img.src = imgData;

                }

            };

 

        })(selectedFile);

        fileReader.readAsDataURL(selectedFile);

 

    };

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


Changes are bolded and italicized. Basically we simply moved name, width and height to become member variables ( which they actually don't really need to be as of right now… they currently aren't used outside the scope of the Done button click handler ), and moved the logic from the img.onload handler to the much more sensible buttonDone click handler.  Not really sure why I didn't do this all along.  Not though, there should really be some error handling/field validation in the click handler as currently there is none, so you could specify a width of "Ralph" if you really wanted to! 

 

Now that the workflow of uploading a sprite sheet is much more sane, we add a few menu items to enable the user to toggle the grid between sizes of 32x32, 64x64 or off completely.  This is done in multiple locations.  First off, the handler in mainMenu.View.js receives all the incoming menu events.  For now we are simply rebroadcasting them so any other view can respond to the fact a menu was selected.

 

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 menuViewGridNone = container.one('#menuViewGridNone');

            menuViewGridNone.on("click",function(e){

                Y.Global.fire('menu:viewGridNone', {

                    msg:"Hello"

                });

            });

 

            var menuViewGrid32 = container.one('#menuViewGrid32');

            menuViewGrid32.on("click",function(e){

                Y.Global.fire('menu:viewGrid32', {

                    msg:"Hello"

                });

            });

 

            var menuViewGrid64 = container.one('#menuViewGrid64');

            menuViewGrid64.on("click",function(e){

                Y.Global.fire('menu:viewGrid64', {

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

 
Cut and paste coding 101!  This code is getting awfully 'samey', so we may want to revisit it with something more elegant in the future as we add more and more menu items.  Then again, if you have done any C++ GUI work, this kind of code is probably looking awfully familiar to you.
 
Oh yeah, we also needed to alter the HTML in or menu template MainMenu.template to add our new menu items.
 

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

 

                <li>

                    <a class="yui3-menu-label" href="#file">View</a>

                    <div id="file" class="yui3-menu">

                        <div class="yui3-menu-content">

                            <ul>

                                <li class="yui3-menuitem" id="menuFileAddSpriteSheet">

                                    <a class="yui3-menu-label" href="#">Grid</a>

                                    <div id="gridMenu" class="yui3-menu"><!-- Bounding box -->

                                        <div class="yui3-menu-content" ><!-- Content box -->

                                            <ul>

                                                <li>

                                                    <a class="yui3-menuitem-content" id="menuViewGridNone" href="#">None</a>

                                                </li>

                                                <li>

                                                    <a class="yui3-menuitem-content" id="menuViewGrid32" href="#">32 x 32</a>

                                                </li>

 

                                                <li>

                                                    <a class="yui3-menuitem-content" id="menuViewGrid64" href="#">64 x 64</a>

                                                </li>

                                            </ul>

                                        </div>

                                    </div>

                                </li>

 

                            </ul>

                        </div>

                    </div>

                </li>

            </ul>

        </div>

    </div>

</div>

 

The syntax can get a bit ugly, but its nothing too outrageous.  Basically we have an LI for our View menu, then within it we are basically creating another menu for the child (pop-out) menu by creating a DIV with the yui3-menu-content style.  We do the same thing again to provide a pop-out menu for the Grid menu item.  Its not as convoluted as it looks, it's mostly a matter of applying the proper CSS class to each item, YUI3 takes care of the rest.

 

OK, now our template is updated and our menu view dispatches an event when the menu items are selecting.  Now lets handle those events, which we will do in editor.View.js which you can think of as our main or parent view.

editor.View.js

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

    Y.EditorView = Y.Base.create('editorView', Y.View, [], {

        initializer:function(){

 

            var person = new Y.Person();

            this.pv = new Y.PersonView({model:person});

            this.menu = new Y.MainMenuView();

            this.map = new Y.MapView();

 

            this.model = this.get('model');

            this.model.on('change',function(e){

                alert("Hi");

            });

 

            Y.Global.on('menu:fileExit', function(e){

               alert(e.msg);

            });

 

            Y.Global.on('menu:viewGridNone', function(e){

                this.map.setGridStyle("None");

 

            },this);

 

            Y.Global.on('menu:viewGrid32', function(e){

                this.map.setGridStyle("32");

 

            },this);

 

            Y.Global.on('menu:viewGrid64', function(e){

                this.map.setGridStyle("64");

 

            },this);

 

            Y.Global.on('menu:fileAddSpriteSheet',function(e){

                var ss = this.model.getSpritesheets();

                var dialog = Y.AddSpriteSheetDialog.show(ss, Y.bind(function(){

                    var sheet = this.model.getSpritesheets().get('spritesheets')[0];

                    console.log(sheet);

                    console.log(this.model.get('name'));

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


We are simply wiring up a event handler that listens to each of our menu events.  In each case they all do the same thing, call the setGridStyle() method of our MapView.  So… let's take a look at map.View.js.


map.View.js


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

     Y.MapView = Y.Base.create('mapView', Y.View, [], {

        grid:null,

        gridSize:"32",

        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;

            this.GridSize = "None";

        },

        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();

 

        },

        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("winWidth");

            var screenHeight = body.get("winHeight");

 

            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;

 

            if(this.gridSize !== "None")

                this.createGrid();

        },

        setGridStyle: function(gridSize){

            this.gridSize = gridSize;

            this.createGrid();

        },

        createGrid:function(){

            if(this.grid !== null)

                this.stage.removeChild(this.grid);

 

            var rows,columns,gridWidth;

 

            if(this.gridSize == "None")

                return;

 

            if(this.gridSize == "32"){

                gridWidth = 32;

                columns = Math.floor(this.stage.canvas.width/gridWidth);

                rows = Math.floor(this.stage.canvas.height/gridWidth);

            }

            else if(this.gridSize == "64"){

                gridWidth = 64;

                columns = Math.floor(this.stage.canvas.width/gridWidth);

                rows = Math.floor(this.stage.canvas.height/gridWidth);

            }

 

            console.log(rows + " " + columns);

            var g = new createjs.Graphics();

            g.setStrokeStyle(1);

            g.beginStroke(createjs.Graphics.getRGB(0,0,255));

 

            //Vertical lines first

            for(var i = 0; i <= columns; i++){

                g.moveTo(i*gridWidth,0);

                g.lineTo(i*gridWidth,this.stage.canvas.height);

            }

 

            //Now the horizontals

            for(var j = 0; j <= rows; j++){

                g.moveTo(0,j * gridWidth);

                g.lineTo(this.stage.canvas.width,j * gridWidth);

            }

 

            this.grid = new createjs.Shape(g);

 

            this.stage.addChild(this.grid);

        }

    });

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



As you can see, map.View.js is where most of the changes have occurred.  Previously it just drew a green ball travelling across the screen.  Now we are getting a bit closer to becoming an application.  It will create a grid on screen that is either off, 32x32 pixels or 64x64 pixels in size.  The vast majority of logic is in the createGrid method.  It starts off by removing the existing grid if it has already been added to our Easel stage.  We then check to see what value has been set for gridSize.  It ifs none, we simply return, which will have resulted in any existing grid being removed and nothing more.  Otherwise we set the gridWidth value to either 32 or 64, and calculate the number of columns and rows that we need to draw based off the value selected ( rounded down ).  Then we simple draw a series of lines, first travelling left to right across the screen, then repeat the process going from top to bottom.  Finally we add our new created grid to the stage.

 

 

At this point, our application is starting to look a bit more like a level editor!

 

 

 

 

You can download the complete updated source code here.

Programming

blog comments powered by Disqus

Month List

Popular Comments