Subscribe to GameFromScratch on YouTube Support GameFromScratch on Patreon

10. July 2013

I'll be the first to admit the expression "cloud computing" is overly abused these days. The reality is, while a bit abused by marketing types, cloud computing is a huge deal for developers.  Gamers simply expect certain functionality in your game that eventually is going to require you to have a server side component.  You may think to yourself…  well, rolling my own server isn't all that difficult.  If you have the skill set to do so, creating say… a server side score board is not all that difficult.  Frankly, it isn't either, in fact I covered exactly that topic in a prior tutorial series.  Easy, right?  Ok… now where are you going to host it and what is that going to cost you?  How secure is it?  Is it encrypted?  How well does it scale?  What about back-ups and fault tolerance?  Who is going to administrate your servers?  What about reporting?  Suddenly this simple little task quickly becomes a much more costly and complicated process and takes time and money away from doing what you set out to do… actually developing a game.

 

At the end of the day, for many developers letting someone else take care of all of these things generally makes a lot of sense.  This is the exact problem that Scoreoid sets out to solve.  You can essentially think of them as a your game's server in the cloud.  Using a fairly simple REST based api you can easily accomplish pretty much anything you could want from a dedicated server and they take care of the infrastructure and day to day operation for you.  In this post we are going to take a closer look at how Scoreoid works and what it does for you.

 

From a developers perspective, Scoreoid can be thought of as a product in two parts.  First you have the developer portal/dashboard where you can monitor, access and maintain your data.  Then you have the REST based API that the programmer uses to work with Scoreoid.

 

WARNING TIME!

To illustrate the various abilities I put together a web application that demonstrates much of Scoreoid's functionality.  This application is hitting live data and you the viewers have the ability to update and create new data.  This means I have NO IDEA what you might encounter when using the application and take no responsibility for anything written.   Please guys, be mature about it.  If you see something hateful or excessively stupid, let me know and I will remove it.

 

The Scoreoid Dashboard

First let's take a quick look at the Scoreoid web interface.  If you haven't already, you need to sign up for a Scoreoid account.  Don't worry, it's completely free and they don't spam you with emails.  Once you are signed up, log in and you will arrive at the main Dashboard page:

 

Dashboard

 

Here you get a top level overview of activity for your game.  In this case you can see that Henry is a very prolific and talented player of Test Game!

Across the top are a series of tabs.  These provide you a way to drill down into data for each category, for example, below is the result of clicking the Player tab.

DashPlayers

 

Here you can modify or delete any value in the Players database.  Scores, Games, Stats and Notifications work similarly, allowing you to access and modify data using a graphic interface.  The one I didn't mention was Console, and this is a handy little tool.  Console allows you to make Scoreoid API calls using a form.

 

ScoreoidConsole

 

Using the Console you pick the API call to make, and the form below is populated with the available parameter fields.  Once you execute it, you see the results below in the results panel, in either XML or JSON format.  You can make use of the entire Scoreoid API using the console.

 

Finally, let's take a look at adding a game.  Click on the Game tab and click the Add New Game button.  You need at least one game to use Scoreoid, but you can of course make many if you wish.  You pass the Game ID as a parameter to a number of the API calls.  Creating a game is simple enough:

CreatingAGame

 

Finally the Help link brings you to the documentation.  You can access the documentation without the need to log in if you want to take a look at the API.  The documentation is complete and has examples in multiple languages for each API call.  Now, let's get down to some coding.

 

 

Programming with Scoreoid

 

Scoreoid is accessed using a REST based interface.  The nice part about a REST based api is, if your application can programmatically access the web, it can use the API.  The actual programming language you use doesn't matter.  In this example, I am going to mostly use JavaScript.  In my case I like working with the YUI library, but you can easily use whatever library you want, the methodology stays very similar although the syntax changes slightly.

 

There are a couple things to note up front.  In the name of keeping the code easy to understand, I've performed a couple programming no-no's in this example.  First I gave you all access to my Scoreoid token…  in your application you obviously would not want to do this.  If you create a JavaScript based Scoreoid application, be sure to read about using an auto generated proxy for securing your application.  Next, I used the HTTP calls instead of the HTTPS ones.  In a secured application, you would want to obviously use the encrypted option.  Finally I made only synchronous networking calls… this means my networking calls cause programming execution to stop until results are returned.  In a real application you would generally want to make asynchronous calls.  As a result of these decisions, the code should however be fairly easy to understand.

 

Let's jump right in to the sample app.  We will be looking at portions of the code as we go.  However a great deal of the code has nothing to do with Scoreoid in particular ( YUI3 or markup related code ), while a lot of it is very repetitious in nature.  Therefore instead of listing it all here, I created a github for it. You can also download a zip of the project here

 

The application requires a server to be run due to the AJAX calls.  Therefore I have included a simple Node based server.  If you have Node installed, you can run the application by typing:

node server.js

Then you can access it in your browser at http://localhost:3000. The vast majority of interesting code for this application is in the views folder, while the templates folder contains the markup. Again, keep in mind this application is working with live data, so play around, but please be mature about it:

 

( Below is the application, not a screen shot :) )

 

 

There is a tab for each area of functionality we want to look at, Game, Player, Score and Cloud.  Under each is a form for interacting with Scoreoid.  At the bottom of each form is a field showing the raw XML or JSON results returned by the server.

 

Before we continue too far, let's take a quick look inside Index.html at the following lines.

        //Global Y variables
        Y["SCOREOID_KEY"] = "bde61648959d0c364b04b93e257035abd5ee3b26";
        Y["GAME_ID"] = "3e99cf43ab"
 

These are two values we will be needing over and over again.  First is my Scoreoid token… you should KEEP YOURS PRIVATE!  The second is the ID of a game I created earlier using the dashboard.  Once again, Scoreoid can support multiple games at once, but in this case I am using only one.  So in the code when you see Y.SCOREOID_KEY and Y.GAME_ID, this is where those values are set.

 

Now let's take a look at the Game tab in the application:

Game

First we will look at the code that executes when the user clicks the Get Game Information button in game.View.js.

        getGameData:function(){
            // This is a blocking web request... in real world, BAD!
            var requestCfg = {
                sync:true,
                data : {
                    api_key: Y.SCOREOID_KEY,
                    game_id: Y.GAME_ID,
                    response:"JSON"
                }
            };
            var request = Y.io("http://api.scoreoid.com/v1/getGame", requestCfg);
 
            Y.Global.fire('displayResults', {msg:request.responseText});
        }
 

Y.io() is what you use to make a HTTP request in YUI3.  The data object within the requestCfg object is where you pass the parameters to Scoreoid.  We pass in our api_key, game_id ( both defined earlier in index.html) as well as response.  The response variable tells Scoreoid what format you want the results returned in, either JSON or XML.  For the vast majority of examples, we are going to go with JSON as it's a bit easier to read and lighter weight.  Finally you call the function in the form of the URL you pass to Y.io, in this case http://api.scoreoid.com/v1/getGame which calls the getGame() method.  There is a wiki page for every API call ( here is getGame for example ), that lists the possible parameter fields as well as the values that will be returned.

 

Click the button and in the results area you should see:

[{"Game":{"user_id":"51b8b1dcfd8978203e000b9d","name":"Test Game","short_description":"This game is so amazingly awesome, it doesn't exist!","description":"Really, it doesn't exist.  The game is a lie.","players_count":6,"scores_count":7,"status":1,"created":"2013-06-12 19:39:07","updated":"2013-06-12 19:39:07"}}]

 

This is the return value of getGame() in JSON format.  JSON is a handy format, as it is basically just a JavaScript object encoded in string from.  You can turn a JavaScript object into JSON by calling JSON.stringify(myObject) and you can turn a JSON string back into a JavaScript object by calling JSON.parse(myString).  Pretty much every programming language under the sun has a JSON parsing library.  For those that don't you can always get Scoreoid to return XML ( we look at an XML example later ).

 

If YUI calling convention is alien to you, and you are more comfortable using jQuery, you can make the above call using the following jQuery code:

$.post("http://api.scoreoid.com/v1/getGame", { api_key:Y.SCOREOID_KEY,game_id:Y.GAME_ID,response:"JSON"},

   function(response) {

     //response will now contain the JSON data returned by Scoreoid

     //note, this is an async call

   });

 

As you can see, it's pretty simple to make REST based calls.  The other two examples in the Game tab are Get Players and Get Highest Gold Amount.  Below is the code for GetPlayers:

        getGamePlayers:function(){
            // This is a blocking web request... in real world, BAD!
            var requestCfg = {
                sync:true,
                data : {
                    api_key: Y.SCOREOID_KEY,
                    game_id: Y.GAME_ID,
                    response:"JSON"
                }
            };
            var request = Y.io("http://api.scoreoid.com/v1/getPlayers", requestCfg);
 
            Y.Global.fire('displayResults', {msg:request.responseText});
        }
 

As you can see the code is basically identical, the only difference is you call a different URL.  getPlayers() returns a list of all of the players of your game, unless of course you tell it to limit the results returned.  Get Highest Gold Amount is also virtually identical:

        getGameTop:function(gameField){
            var requestCfg = {
                sync:true,
                data : {
                    api_key: Y.SCOREOID_KEY,
                    game_id: Y.GAME_ID,
                    response:"JSON",
                    field:gameField
                }
            };
            var request = Y.io("http://api.scoreoid.com/v1/getGameTop", requestCfg);
 
            Y.Global.fire('displayResults', {msg:request.responseText});
 
        }
 

Once again, we call a different URL. In this case we are also passing in an additional parameter field.  Field tells the method which value we want to get the highest value of when calling getGameTop.  As you can see when we called getGameTop(), the value that was passed in was gold.

            container.one("#buttonGetWealthiest").on("click",function(){
                this.getGameTop("gold");
            },this);
 
There are a number of valid options for field ( bonus, best_score, gold, money, kills, lives, time_played, unlocked_levels ).  Once again, you can get a list of valid options in the documentation for getGameTop.  If you press the Get Highest Gold Amount button, you should see:
 
GetHighestGold
 
Of course, since you guys have full access to all the data in the application, the actual value returned might change.
 
Now let's take a look at working with Player data:
 
 
Player
 
The code behind this form is player.View.js.
 
Creating a player is once again a very straight forward process.  First the code that is called when the user clicks the Create Player button:
 
            this.get('container').one("#buttonCreatePlayer").on("click",function(){
                this.createPlayer(Y.one('#playerName').get('value'));
            },this);
 
 

This simply gets the value of the playerName text field ( see player.Template in the templates folder for HTML markup ) and passes it to the function createPlayer().  Now let's look at createPlayer:

 

        createPlayer:function(playerName){
            var requestCfg = {
                sync:true,
                data : {
                    api_key: Y.SCOREOID_KEY,
                    game_id: Y.GAME_ID,
                    username: playerName,
                    response:"JSON"
                }
            };
            var request = Y.io("http://api.scoreoid.com/v1/createPlayer", requestCfg);
            Y.Global.fire('displayResults', {msg:request.responseText});
        }
 

Once again, the actual code remains almost identical.  The URL we are calling is now http://api.scoreoid.com/v1/createPlayer and we pass the new player's name in the field username.

 

Next we will look at working with XML data instead.  If you click the Get All Players button, the following grid will appear:

GetPlayers

 

The YUI grid binds to an XML results set, so this time we are going to call /getPlayers, but instead we want it to return XML results instead of JSON.  This code is going to look a bit more daunting then it really is.  This is what executes when the user presses Get Players:

this.get('container').one("#playerGetAll").on("click",function(){
 
                var requestCfg = {
                    method:"POST",
                    data : {
                        api_key: Y.SCOREOID_KEY,
                        game_id: Y.GAME_ID,
                        response:"XML"
                    }
                };
 
                var ds = new Y.DataSource.IO({source:"http://api.scoreoid.com/v1/getPlayers"});
                ds.plug(Y.Plugin.DataSourceXMLSchema, {
                        schema: {
                            resultListLocator: "player",
                            resultFields: [
                                { key:"Username", locator:"@username" },
                                { key:"Email", locator:"@email" },
                                { key:"TimePlayed", locator:"@time_played" },
                                { key:"Gold", locator:"@gold" },
                                { key:"BestScore", locator:"@best_score" }
                            ]
                        }
                    });
                ds.sendRequest({
                    callback:{
                        success: function(e)
                            {
                                var dt = new Y.DataTable(
                                    {
                                    columns:[{key:"Username"},{key:"Email"},{key:"TimePlayed"},{key:"Gold"},{key:"BestScore"}],
                                    data: e.response.results,
                                    summary:"List of all players in the game",
                                    caption:"Players"
                                    });
                                // Clear the table, in case one already exists
                                Y.one("#dataTable").setContent("");
                                // Now populate
                                dt.render(Y.one("#dataTable"));
 
                                Y.Global.fire('displayResults', {msg:e.data.response});
                            },
                        failure: function(e){
                            Y.log(e);
                        }
                    },
                    cfg:requestCfg
                });
 
            },this);
 

Once again we are making a call to the getPlayers url. Notice however that we set Response to XML.  This time instead of getting data using Y.io(), we instead want to populate an XML dataset using Y.DataSource.IO().  The actual call is made when we call sendRequest and we pass in the requestCfg here.  The remaining code is about selecting which fields from the XML we want to have displayed in the grid.

 

Speaking of XML, here is the results of getPlayers() in XML form:

<?xml version="1.0" encoding="UTF-8"?>
<players>
<player username="Henry" unique_id="" first_name="" last_name="" email="mike@isawesome.com" bonus="0" achievements="" gold="9000" money="0" kills="0" lives="0" time_played="300" unlocked_levels="" unlocked_items="" inventory="" last_level="" current_level="" current_time="0" current_bonus="0" current_kills="0" current_achievements="" current_gold="0" current_unlocked_levels="0" current_unlocked_items="" current_lives="0" xp="" energy="" boost="" latitude="" longtitude="" game_state="" platform="" rank="0" best_score="90003" created="2013-06-21 16:39:10" updated="2013-06-24 16:33:05">
</player>
<player username="Mike" unique_id="" first_name="" last_name="" email="" bonus="0" achievements="" gold="4512" money="0" kills="0" lives="0" time_played="42" unlocked_levels="" unlocked_items="" inventory="" last_level="" current_level="" current_time="0" current_bonus="0" current_kills="0" current_achievements="" current_gold="0" current_unlocked_levels="0" current_unlocked_items="" current_lives="0" xp="" energy="" boost="" latitude="" longtitude="" game_state="" platform="" rank="0" best_score="0" created="2013-06-21 16:27:33" updated="2013-06-25 15:56:07">
</player>
------------------------------ RESULTS SNIPPED ------------------------------------
</players>

 

As you can see, there is a great deal more information available than what we are choosing to display.  Switching between JSON and XML results is a trivial task.

 

The final part of the Player page illustrates how to retrieve and update a single field of an individual player.  The code is pretty much identical to what we have seen thus far.  See the getPlayerField() and setPlayerField() functions for more details of how this code works.  The fields in the um… Field drop down is set in the players.Template code.  It is simply a combo box filled with the available values.  Once again, valid values can be obtained  from the documentation. 

 

One of the most common tasks that game programmers require a server for is online leader boards.  Making an online scoreboard with Scoreoid is a pretty simple affair, as we will see looking at the Scores tab:

 

Score

 

The code controlling this form is available at score.View.js.  Using this form you can get high scores, get scores sorted high to low and low to high, get the average over all score and create a new high score.  At this point in time, the code should be intimately familiar, as the logic is identical, just the URLs and parameters change.  The first three buttons call the method /getScores.  The only difference is for the sorted results we pass the parameter order_by with the value either "asc" for ascending or "desc" for descending results.  Get Average Score calls the method /getAverageScore ( predictably enough… ).  The /getScores method has a number of parameters we didn't use here for fine tuning your results.  You can fine tune the results by specifying difficulty level, date range, platform as well as limit your results to N number of values returned.  In the end, implementing a complete scoreboard is pretty simple.

 

Finally, let's take a look at the Cloud tab.

 

Cloud

 

Sometimes you just need to store "stuff" ( technical term there! ) in the cloud.  This is a key/value database stored at the game level ( more globally ) instead of the player or score level.  Most of the code here makes use of the /getGameData() and /setGameData() methods.  You can access the code behind this form in cloud.View.js.

 

If for example you click the Set Cloud Data, the value you pass is going to be written to the hard coded key "myData".  Perhaps the most interesting aspect of Scoreoid cloud storage is it supports JavaScript "dot" notation.  So for example, you can set data for monster, such as monster:orc, but also like monster.hp = 42.  Here for example are the current results in JSON format if you hit Get Monster from the cloud:

 

{"monster":{"alignment":"Lawful Evil","maxHitPoints":"42"}}

 

This makes storing structured hierarchical data easy.

 

So, that's the basics of using Scoreoid.  The API is remarkably consistent, so once you've made a single call you pretty much know how to use the entire API.  The rest is a matter of looking at the documentation for what parameters and results you can expect.  The rest is just a matter of parsing the results that are returned.

 

As I mentioned earlier, a REST based API is usable by any programming language capable of making programmatic web requests.  So far, I've entirely focused on JavaScript, but now I will give an example using a different programming language.  In this case, C# for the PlayStation Mobile platform.  The following example shows how you would access a scoreboard in a PS Vita application.  Since the PSM SDK doesn't included a JSON parser, but does include an XML one, I've gone with XML for the return value.

 

 

 

using System;
using System.Collections.Generic;
using Sce.PlayStation.Core;
using Sce.PlayStation.Core.Graphics;
using Sce.PlayStation.Core.Input;
using Sce.PlayStation.HighLevel.UI;
using System.Net;
using System.Linq;
using System.Xml.Linq;
using System.IO;

namespace Scoreoid
{
	public class AppMain
	{
		private static GraphicsContext graphics;
		private const string SCOREOID_KEY = "bde61648959d0c364b04b93e257035abd5ee3b26";
		private const string GAME_ID = "3e99cf43ab";
		
		public class ScoreEntry
		{
			public string Name;
			public string Score;
		}
		public static void Main (string[] args)
		{
			graphics = new GraphicsContext();
			UISystem.Initialize(graphics);
			
			var scene = new Sce.PlayStation.HighLevel.UI.Scene();
			var panel = new Panel() {
				Width = graphics.Screen.Width,
				Height = graphics.Screen.Height };
			
			
			var textOut = new Label();
			textOut.Width = panel.Width;
			textOut.Height = panel.Height;
			textOut.HorizontalAlignment = HorizontalAlignment.Center;
			textOut.Text = "High Scores Go Here";
			panel.AddChildFirst(textOut);
			scene.RootWidget.AddChildFirst(panel);
			
			var request = HttpWebRequest.Create (@"http://api.scoreoid.com/v1/getScores?response=XML&api_key=" +
			                                     SCOREOID_KEY +
			                                     "&game_id=" +
			                                     GAME_ID);
			request.ContentType = "application/xml";
			request.Method = "GET";
			
			using (HttpWebResponse response = request.GetResponse() as HttpWebResponse) {
				if (response.StatusCode != HttpStatusCode.OK)
					Console.Out.WriteLine("Error fetching data. Server returned status code: {0}", response.StatusCode);
				else {
					using (StreamReader reader = new StreamReader(response.GetResponseStream())) {
						var content = reader.ReadToEnd();
						if (string.IsNullOrWhiteSpace(content)) {
						System.Diagnostics.Debug.WriteLine("Scoreoid returned no results");
						}
						else {
							XDocument doc = XDocument.Parse(content);
							var scores = (from e in doc.Root.Elements("player").Elements("score")
							select new ScoreEntry {
								Name= (string)e.Parent.Attribute("username"),
								Score= (string)e.Attribute("score")
							}).ToList();
							
							string text = "";
							foreach (var score in scores) {
								text += score.Name + " " + score.Score + "\n";
							}
							
							textOut.Text = text;
						}
					}
				}
			}
			
			UISystem.SetScene(scene);
			
			while(true){
				graphics.Clear ();
				UISystem.Update(Touch.GetData(0));
				UISystem.Render ();
				graphics.SwapBuffers();
			}
		}
	}
}

 

If you run this code, you will see:

ScreenShot

 

If you look closely at the code, the actual Scoreoid calls take only a few lines of code.  You create a HttpWebRequest passing the URL as well as the parameters in the URL ( this time we are doing a GET request instead of a POST request ).  Then its simply a matter of getting the results using GetResponse().  The remaining code is about parsing the XML results and PSM specific display code.

 

 Other Info

 

There are a few downsides to outsourcing your game server that you need to be aware of up front.  What happens if something happens to your provider or you want to switch providers?  Here is Scoreoid's policy on data from their website:

With Scoreoid your data belongs to you, we respect that you retain ownership of your data. Scoreoid isn’t another game network taking away your most valuable asset your players and game data. Our bulk storage engine is MongoDB, our preferred method of returning your data to you is via zipped ‘mongodump’.

MongoDB is a freely available open source NoSQL database with binaries available on Windows, Mac, Linux and Solaris. 

The next most obvious question… what does it cost?

This one is a bit trickier to answer, as as of right now, the pricing hasn't been set.  The good new is, right now it's completely free!  There are a number of live games currently using Scoreoid.  That then leaves the question, what will it cost?  For that, we go to the FAQ:

How much will the pro accounts cost?

We're currently focusing on adding new features and we're still working on the pro account options of course there will always be a freemium option.

Scoreoid's mission statement is to provide a free version to every developer which includes updates and new features. We believe every game developer should have an option to use Scoreoid helping them save time and cost while allowing them to improve there game titles and brand.

As for pricing we will offer a number of plans that will fit every developers budget. Currently looking at having 3 main plans between $15 to $50 per month and then a 4th enterprise plan for big publishers and big game studios.

We're also looking at the possibility of have a $5/$8 month option for developers who use very little data or bandwidth.

Our goal is be very cost effective and help game developers, most users will be fine with the free or lower end plan. Transparence is very important to us especially with our pricing model we will notify all users in advanced before we active the pro options.

We also plan on doing active surveys before we activate the pro plans as we would like to get as much feedback as possible from our users giving you a chance for direct input and influence.

 

I have been using Scoreoid since it launched what will happen when pro accounts are activated?

All current users who are using Scoreoid for active games (who have at least one live game that is using Scoreoid) and decide to upgrade to a pro account will receive major discounts and additional benefits. We will also include you in our "loyalty program".

We will keep your current plan and limits for set time limit grandfathering you into a new plan sets. Don't forgot Scoreoid will always offer a freemium option.

 

So basically, the prices aren't set yet, they will be tiered from free on up and if you publish now you will get a discount once pricing goes live. Perhaps most important of all for smaller publishers, there will always be a free tier.

 

So if you are looking at adding server side functionality to your game and don't want to roll your own solution, consider checking out Scoreoid.

Programming , , ,

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

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 ,

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

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

Month List

Popular Comments