Click here to Skip to main content
15,867,453 members
Articles / Web Development / HTML5

Editor for Mario5

Rate me:
Please Sign up or sign in to vote.
4.98/5 (62 votes)
2 Aug 2012CPOL22 min read 104.7K   3K   90   51
Adding some spice to the Mario game by providing a Level editor with a social platform.

Super Mario for HTML5

Introduction

This is the follow up article on the Mario5 game. While the first article focused on the coding technique and the principle design, this article will focus on how to re-use code and integrate existing code. This article will hopefully show that JavaScript code, which is written in an atomic manner, can actually be easily maintained and extended. The integration into existing code bases is also possible.

The first article on the Mario5 game introduced the concept of a class based JavaScript approach. This article assumes you've read the article about the Mario5 game on the CodeProject. We will not discuss the class based JavaScript approach, nor will we go into details of the class diagram again of the game again. We have already set up a (maybe not ideal, but kind of easy to write) format for levels. Since hand-writing levels is a tedious and non-visual (therefore maybe a error-prone) task, we want to create a proper level editor in the browser. Saving the level to the file system is not (directly) possible, therefore we will also look into integrating the existing code base into a web application.

Background

If you haven't played the Mario game yet, then check out the video on YouTube. In the video you will see the final outcome of the first article in action. The two students of mine, which started the Mario project and did all the artwork (partly completely themselves, partly by finding free artwork on the Internet and compiling those into sprite sheets), did supply a level editor. Since we rewrote the game completely, embracing an object oriented approach, we will now try to re-use the classes we've written for the editor. Therefore the old editor - which was written well and contained features like undo, a visual grid and others - will not be presented here. Instead we will write the editor completely new using the same principles as in the first article.

Using the basic design of the game for extensions

We can re-use existing objects to construct new and different ones. We've seen this principle by creating the StaticPlant and PipePlant classes. Those classes are inheriting from the Plant class. Both classes are therefore quite similar (they share some properties), but in their appearance completely different.

The example above is quite simple in its nature, however, the same principle can be applied when modifying the Level class. Let's consider the following code:

JavaScript
var Editor = Level.extend({
    init: function(id) {
        this.world = $('#' + id);
        this.grid = false;
        this.setPosition(0, 0);
        this.reset();
        this.undoList = [];
    }
});

Here we do not call the constructor (by using this._super()) of the Level class. Like a Level objects expects an ID of the container for the level to be passed, the Editor expects also an ID to be passed. We already see editor specific properties being initialized like the grid and the undoList. Starting with the principle we can now alter and extend the possibilities of the Level class even more:

JavaScript
var Editor = Level.extend({
    init: function(id) {
        var me = this;
        this.world = $('#' + id);
        this.grid = false;
        this.setPosition(0, 0);
        this.reset();
        this.undoList = [];
    },
    reset: function() {
        this._super();
        $('<canvas />').addClass('grid').appendTo(this.world);
        var data = [];
        
        for(var i = 100; i--; ) {
            var t = [];
            
            for(var j = 15; j--; )
                t.push('');
            
            data.push(t);
        }
        
        this.load({
            height: 15,
            width: 100,
            background: 1,
            id: 0,
            data: data
        });
    },
    save: function() {
        return JSON.stringify(this.raw);
    },
    setSize: function(w, h) {
        this._super(w, h);
        this.generateGrid();
    },
    setImage: function(index) {
        if(this.raw)
            this.raw.background = index;
            
        this._super(index);
    },
    generateGrid: function() {
        var c = $('.grid', this.world).get(0).getContext('2d');
        c.canvas.width = this.width;
        c.canvas.height = this.height;
        c.clearRect(0, 0, c.canvas.width, c.canvas.height);
        
        if(this.grid) {
            for(var i = 32; i < this.width; i += 32) {
                c.moveTo(i, 0);
                c.lineTo(i, 480);
            }
            
            for(var i = 32; i < 480; i += 32) {
                c.moveTo(0, i);
                c.lineTo(9600, i);
            }
            
            c.lineWidth = 0.5;
            c.strokeStyle = '#FF00FF';
            c.stroke();
        }
    },
    start: function() {
        //Left blank intentionally...
        //This is just to override (and disable) the parent start();
    },
    pause: function() {
        //Left blank intentionally...
        //This is just to override (and disable) the parent pause();
    },
    gridOn: function() {
        this.grid = true;
        this.generateGrid();
    },
    gridOff: function() {
        this.grid = false;
        this.generateGrid();
    },
    setParallax: function() {
        //Left blank intentionally...
        //This is just to override (and disable) the parent setParallax();
    },
});

This is already a quite awesome state. If we would now start the game we would generate our level (and could even have a grid to show the 32px x 32px blocks, that each level is consisting of). To see this we will also need some HTML again. Let's start with the following HTML to provide a proper container for our level editor:

HTML
<!doctype html>
<html>
<head>
<meta charset=utf-8 />
<title>Super Mario HTML5 Editor</title>
<link href="Content/style.css" rel="stylesheet" />
</head>
<body>
<div id="edit">
<div id="edit_world"></div>
</div>
<div id="tool">
<div id="tool_world"></div>
</div>
</div>
<script src="Scripts/jquery.js"></script>
<script src="Scripts/testlevels.js"></script>
<script src="Scripts/oop.js"></script>
<script src="Scripts/constants.js"></script>
<script src="Scripts/main.js"></script>
<script src="Scripts/editor.js"></script>
<script>
$(document).ready(function() {
    var edit = new Editor('edit_world');
    edit.load(definedLevels[1]);
    edit.gridOn();
});
</script>
</body>
</html>

Here we are re-using a lot of scripts that have been written in the first article. Only the editor.js file is new. The file will contain all the JavaScript we are going to write in this article (well, to be honest and more correct: most of it). The style.css file is basically the same as in the first article. We just need a few additional statements:

CSS
#edit {
	height: 480px; width: 640px; position: absolute; left: 50%; top: 50%;
	margin-left: -321px; margin-top: -241px; border: 1px solid #ccc; overflow: hidden;
}
#tool {
	width: 640px; position: absolute; left: 50%; top: 50%; margin-left: -321px;
	margin-top: 282px; height: 128px; background: #ddd; border: 1px solid #ccc;
}
#tool_world { 
	margin: 0; padding: 0; position: relative; top: 0; left: 0; height: 100%; width: 100%;
}
.tool {
	margin: 0; padding: 0; z-index: 99; position: absolute;
}
.grid {
	margin: 0; padding: 0; z-index: 150; position: absolute; top: 0; left: 0;
}
.block {
	z-index: 100;
}

Most of those rules are unnecessary for now, but will become essential later. Right now we just need the additional rule for the element with the #edit selector. This rule is the same as for the #game selector in the first article.

Until now all we did was showing a part of the level. Before we introduce the possibilities of adding and removing items, we should somehow introduce the possibility to scroll (horizontally). We could do this with a scrollbar provided from the browser - a simple change in the CSS rules would be enough. However, must browsers add additive scrollbars, i.e. for a horizontal scrollbar we must therefore add a certain amount of pixels in height (which is about 20px). We would like to use a different scrollbar system, so that we will still just have the 480px as total height.

We could write such a scrollbar control on our own, but to save time (and perhaps money) we pick a solution to suits our fits. The solution should be available as a jQuery plug-in, since we are already using jQuery and do not want to have extra overhead or complications with another library like Prototype, MooTools, or others. There are several scrolling plug-ins available for jQuery, but most of them just fit for vertical scrolling (which is the most common case). After a long search we eventually end up with slimScroll, which is available at the author's website.

This plug-in is only made for vertical scrolling, since the author thinks that horizontal scrolling is a stupid thing and should never be wanted (apparently he did not know about the Metro design principles or our Mario level editor!). The restriction to horizontal scrolling can be modified by changing all lines in the plug-in from the horizontal to the vertical part and vice versa. Showing the modified code here would be a waste of space, since the modifications are really small. Also the modified plug-in is available in the download package.

Finally we can modify the constructor of the Editor class by adding the following call in the constructor:

JavaScript
var Editor = Level.extend({
    init: function(id) {
        /* as before */
        this.world.slimScroll({ height : 480 })
    },
    /* rest as before */
});

Now we can already see that the level being created completely.

The level editor, toolboxes and macro elements

At this point we are able to see and scroll in the level we've decided to edit. What we did not include until this point is the ability to add new blocks or erase existing blocks. Therefore we will have to modify the code even further. Our first modifications will focus on adding some more classes to our application. First of all we create a class that will play an important role as a container for items to be added to the level. We will call this class ToolBox.

JavaScript
var ToolBox = Level.extend({
    init: function(id, edit) {
        this.world = $('#' + id);
        this.edit = edit;
        this.setPosition(0, 0);
        this.reset();
        this.world.slimScroll({height: 128});
    },
    load: function(names) {
        var x = 0;
        this.obstacles = [];
        
        for(var ref in reflection) {
            if(!names || names.indexOf(ref) !== -1) {
                this.obstacles.push([]);
                var t = new (reflection[ref])(x, 0, this);
                t.view.addClass('block').draggable({
                    stack: false,
                    cursor: 'move',
                    cursorAt: { top: t.height / 2, left: t.width / 2 },
                    opacity: 0.8,
                    distance: 0,
                    appendTo: 'body',
                    revert: false,
                    helper: 'clone',
                }).data('name', ref);
                x += t.width + 2 * (t.x - x);
            }
        }
    },
    getGridHeight: function() {
        return 1;
    },
    getGridWidth: function() {
        return this.obstacles.length;
    },
    start: function() {
        //Left blank intentionally...
        //This is just to override (and disable) the parent start();
    },
    pause: function() {
        //Left blank intentionally...
        //This is just to override (and disable) the parent pause();
    },
});

The class also inherits from the Level class as the Editor did. We are also overriding some methods with dummy methods, to prevent usage of inappropriate stuff. An important feature lies in the load() method. Here we allow an optional argument called names to be passed. If this argument is passed, an array with names of items to be added to this ToolBox instance will be expected. This will be an important option if we want to have separate toolboxes later on. If the argument has not been set, all available items will be added.

The next class we build is a common base for objects that are only available in the editor. We start of by providing a common base for such items. In order to provide classes for this we will first create a base class to end up with less code repetitions. This new base class should be called ToolBoxBase:

JavaScript
var ToolBoxBase = Base.extend({
    init: function(x, y, level) {
        this.view = $(DIV).addClass(CLS_TOOL).appendTo(level.world);
        this._super(x, y);
        this.level = level;
    },
    addToGrid: function(x, y) {
        this.level.obstacles[x / 32][14 - y / 32] = this;
    },
    onDrop: function(x, y) {
        //Do nothing here by default ...
    },
    setImage: function(img, x, y) {
        this.view.css({
            backgroundImage : img ? c2u(img) : 'none',
            backgroundPosition : '-' + (x || 0) + 'px -' + (y || 0) + 'px',
        });
        this._super(img, x, y);
    },
    setPosition: function(x, y) {
        this.view.css({
            left: x,
            bottom: y
        });
        this._super(x, y);
    },
    setSize: function(w, h) {
        this._super(w, h);
        this.view.css({
            width: w,
            height: h
        });
    },
});

The basic structure of this class follows the creations of classes like Enemy and Hero. We do not need a move() method here, since items inheriting from ToolBoxBase will be only used in the level editor.

The first item that can be derived from ToolBoxBase is a rubber, i.e., an item that functions as an eraser for already added items. We will call this class just ToolBoxEraser and override the mandatory onDrop() method. Here we use the information of x and y to erase the item at the provided location from the level instance (which is in fact our editor). We return true to signal the calling function that no further steps are necessary.

JavaScript
var ToolBoxEraser = ToolBoxBase.extend({
    init: function(x, y, level) {
        this._super(x, y, level);
        this.view.css('border', '1px solid #000');
        this.setSize(32, 32);
    },
    onDrop: function(x, y) {
        this.level.setItem('', x, y);
        this.view.remove();
        return true;
    },
}, 'Eraser-1x1');

Additionally we want to have the possibility of adding some macro building blocks. Such blocks will consist of existing blocks and should speed up the level creation. We name the base class for all such objects ToolMulti and derive again from the ToolBoxBase.

JavaScript
var ToolMulti = ToolBoxBase.extend({
    init: function(x, y, level, width_blocks, height_blocks, master) {
        this._super(x, y, level);
        this.master = master;
        this.width_blocks = width_blocks;
        this.height_blocks = height_blocks;
        this.setSize(width_blocks * 32, height_blocks * 32);
    },
    onDrop: function(x, y) {
        this.view.remove();
        return false;
    },
});

Here the onDrop() provides the functionality for all classes, that will be representatives from the macro items group. The basic functionality is that the item's view is removed once it's dropped on the level. We return false to signal the calling function that adding the item to the level is still necessary.

How does creating a macro class for usage in the level editor now look like exactly? Due to our efforts in a proper object oriented hierarchy we do only need to call the base constructor with the appropriate arguments. Finally we give the class a unique reflection name - so that it will be added to the list of creatable objects. This also allows us to add it to a toolbox with specific elements only.

JavaScript
var ToolMultiSoil2 = ToolMulti.extend({
    init: function(x, y, level) {
        this._super(x, y, level, 2, 2, 'soil');
        this.setImage(images.objects, 1071, 3);
    },
}, 'Soil-2x2');

Now that we have created the platform for having objects to add and erase, we only need to add the proper functionality in the Editor class. We have been already using methods, e.g. the setItem() method in the onDrop() function of the ToolBoxEraser class, which do not exist at the moment. Now we have to add these methods with the proper implementations.

Since our macro objects will be width x height arrays of existing objects, we will need a more general setItems() method. Also we need wrappers around these functions, which perform some checks before actually calling setItem() or setItems(). The checks should involve scenarios like the addition of a second Mario (there can only be one player!), if the proposed position is valid and if the onDrop() method did return true as value.

JavaScript
var Editor = Level.extend({
    /* Existing methods */    
    setItem: function(value, x, y, noUndo) {
        this.setItems(value, [x], [y], noUndo);
    },
    setItems: function(value, xs, ys, noUndo) {
        var t = [];
        
        for(var i = 0, n = xs.length; i < n; i++) {
            t.push({
                name: this.raw.data[xs[i]][ys[i]],
                x: xs[i],
                y: ys[i]
            });
            
            this.raw.data[xs[i]][ys[i]] = value;
        }
        
        if(!noUndo)
            this.pushUndoList(t);
    },
    addItem: function(name, x, y, noUndo) {
        if(name === 'mario' && this.mario) {
            var oldx = this.mario.i;
            var oldy = this.mario.j;
            this.mario.view.remove();
            this.setItems(['', 'mario'], [oldx, x], [oldy, y]);
            new (reflection[name])(32 * x, 448 - 32 * y, this);
            return;
        }
        
        this.removeView(x, y);
        var t = new (reflection[name])(32 * x, 448 - 32 * y, this);
        
        if(t.onDrop && t.onDrop(x, y))
            return;
            
        var xarr = [];
        var yarr = [];
        
        if(t.width_blocks && t.height_blocks && t.master) {
            var w2 = t.width_blocks / 2;
            var h2 = t.height_blocks / 2;
            name = t.master;
            
            for(var xi = Math.ceil(x - w2); xi < Math.ceil(x + w2); xi++) {
                for(var yi = Math.ceil(y - h2); yi < Math.ceil(y + h2); yi++) {
                    xarr.push(xi);
                    yarr.push(yi);
                    this.removeView(xi, yi);
                    new (reflection[name])(32 * xi, 448 - 32 * yi, this);                
                }
            }
        } else {
            xarr.push(x);
            yarr.push(y);
        }
        
        this.setItems(name, xarr, yarr, noUndo);
    },
    removeItem: function(x, y, noUndo) {
        this.removeView(x, y);
        this.setItem('', x, y, noUndo);    
    },
    removeView: function(x, y) {
        if(this.obstacles[x][y])
            this.obstacles[x][y].view.remove();
        else {
            for(var i = this.figures.length; i--; ) {
                var gp = this.figures[i].getGridPosition();
                
                if(gp.i === x && gp.j === y) {
                    this.figures[i].view.remove();
                    this.figures.splice(i, 1);
                    break;
                }
            }
        }
    },
});

The method removeItem() deals with the case of actually removing an item from the level array. We also added a helper method to remove the view of the item, which is about to be removed from the array.

Finally we want to add a working undoList. We already prepared the array, and added some method call in the setItems() method. We do not need much code here, just a few functions to organize the list and a method to actually invoke an undo action.

JavaScript
var Editor = Level.extend({
    /* Existing methods */    
    pushUndoList: function(action) {
        this.undoList.push(action);
    },
    popUndoList: function() {
        return this.undoList.pop();
    },
    undo: function() {
        if(this.undoList.length) {
            var action = this.popUndoList();
            
            for(var i = 0, n = action.length; i < n; i++) {
                var x = action[i].x;
                var y = action[i].y;
                
                if(action[i].name)
                    this.addItem(action[i].name, x, y, true);
                else
                    this.removeItem(x, y, true);
            }
        }
    },
});

Now our editor is complete and can be used by some script. A demo of the editor is included in the download package. Basically the demo is build up the same way that the original game demonstration was built up.

Building a platform around the game

Maintaining a big JavaScript project is as tedious as maintaining other big projects. Therefore we need to split up the code in smaller projects, which do not have any dependencies (in the best case). What have we built so far?

  • An abstract layer for giving us the impression of true object oriented code in JavaScript, named (1)
  • A sound manager project, depending on (1), named (2)
  • A keyboard (or general input) project, depending on (1), named (3)
  • The game itself (base, level, some objects, ...), depending on (1), named (4)
  • The level editor with some new objects, depending on (1) and (4)

Additionally the game itself always (2) and (3) to be plugged in, forming a controllable game with sound. We also did not mention that our game requires jQuery as an additional JavaScript layer (simplifies cross-browser programming and provides some time-saving features). The editor also requires the jQuery slimScroll plug-in, which does require a part of jQuery UI (in order to be draggable and more). Overall we have to following external dependencies:

  • jQuery
  • jQuery UI (custom built with some features is enough - no theme required)
  • jQuery slimScroll

Now for any application using the Mario5 game source we need at least jQuery and the OOP layer. Since we are interested in controlling the game we also need an implementation of the keyboard class. If we want to have some sound we should also include a proper implementation of the sound class.

If our application should provide the Mario5 level editor we need more sources. Additionally to the dependencies of the Mario5 game (and the game itself), we need jQuery UI and the jQuery slimScroll plug-in. Providing all those scripts builds the bases for any web application.

Our goal now is to built a game with some community orientation around the Mario5 game. First of all we should have a vision about the included features:

  • Registration and login
  • Editing, Saving and loading levels
  • Playing the single-player campaign as well as playing provided custom levels
  • Rating levels of other authors (this is very CodeProject like!)

Those are basically all functions that need to be provided. All in all we have a really simple database. The code first approach to the database can be displayed as the following diagram:

The database concept

The related code of the specific DbContext implementation looks like the following:

C#
namespace SuperMario.Models
{
    public class MarioDB : DbContext
    {
        public DbSet<Level> Levels { get; set; }
        public DbSet<User> Users { get; set; }
        public DbSet<Rating> Ratings { get; set; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Conventions.Remove<IncludeMetadataConvention>();
            modelBuilder.Entity<User>().ToTable("My_AspNet_Users");
        }

        public void Detach(object entity)
        {
            var objectContext = ((IObjectContextAdapter)this).ObjectContext;
            objectContext.Detach(entity);
        }
    }
}

Since we are using the Entity Framework we can use it with any database provider by just setting the appropriate one in the web.config file.

We still have to write some actions. We do not want to go into details of every implementation here, so we just show the implementation of the load and save level here (since this is a direct (and dependent) JavaScript / our Mario5 game module). First of all, what do we need to actually save a level?

  • A button that triggers the event
  • A JavaScript event handler
  • A call to the jQuery ajax() method, or a more specialized wrapper
  • A proper action as target URL

Now that we have everything together we just have to implement the action:

C#
namespace SuperMario.Controllers
{ 
    public class LevelController : Controller
    {
        /* ... */

        //
        // GET: /Level/Save

        [Authorize]
        public ActionResult Save()
        {
            return PartialView();
        }

        //
        // POST: /Level/Save

        [HttpPost]
        [Authorize]
        public ActionResult Save(Level level)
        {
            if(level == null)
                return PartialView();

            var content = H.Json.Decode<LevelData>(level.Content);
            level.UserId = (int)Membership.GetUser().ProviderUserKey;
            level.Played = 0;
            level.Created = DateTime.Now;
            level.Updated = DateTime.Now;
            level.Background = content.background;

            if (ModelState.IsValid)
            {
                db.Levels.Add(level);
                db.SaveChanges();
                content.id = level.Id;
                level.Content = H.Json.Encode(content);
                db.Entry(level).State = EntityState.Modified;
                db.SaveChanges();
                return Json(new { id = level.Id });
            }

            return PartialView(level);
        }
    }
}

Actually we wrote two methods, one (the form) being triggered when the user hits the button (over an AJAX request) and one being triggered when the user submits the form. In this method we are doing some basic model creation and some model update. Since the level array will be stored as a JSON string containing the used ID, we need to modify the ID. This is a problem, since we do not know the ID before the actual insertion. We can walk around this problem by doing it in two steps:

  1. First we insert the entity into the database (here the SaveChanges() call is important)
  2. Then we update the JSON string by deserializing it, updating the property, serializing it again and updating the entity

Additionally we also save some extracted information from the JSON string. This is performed to save some computation power later, when a list of existing levels is requested. We should note that we have a direct DbContext access here, which should be avoided in big web applications. For our small web app surrounding the Mario5 game this is still acceptable.

The JavaScript of the web application needs to wire up this action to a proper button, which is responsible for saving the current level. Here we have to distinguish between saving a new level and saving an existing one, i.e. editing one.

JavaScript
$('#saveEdit').click(function () {
    var url = edit.id ? ('/Level/Edit/' + edit.id) : '/Level/Save';
    webapp.performAjax(url, function () {
        $('#Content').val(edit.save());
    });
});

This code snippet wires up the button with the ID saveEdit with the proper AJAX call. If the editor has a valid ID assigned, we use the URL to the edit action, otherwise we use the action we've shown above.

Social integration and mobile considerations

We need to think of different (those are maybe current trends, but IT is always about current trends) ways to make our application usable and known:

  • Using share buttons for allowing users to spread the word easily
  • Using OpenAuth for allowing users to make use of their (primary) online account
  • Integrating touch friendly buttons (and wiring them up to the touch control) for controlling the game
  • Making the game playable on mobile devices like Smart Phones

The share buttons will be taken from a page called Shareaholic.com. On this page we can compile our own set of social bookmarks. Once we are finished the page gives us a snippet, which has to be included at our desired location within the page. Of course we will include the big three (Facebook, Twitter, and Google+), but also Orkut, LinkedIn, or more traditional services like sharing by E-Mail.

Some page content does not need the user's full attention and should be considered additional to the usual content. Such content can be delivered by using modeless, i.e., non-blocking, dialog boxes. The principle of such dialogs is that they can be filled with whatever content and do not depend on the current page. In the end such dialogs will look like this image:

The modeless dialog in action

Including an OpenAuth provider can be tricky, but lucky for us most of the work can be done by the DotNetOpenAuth library (hosted at dotnetopenauth.net). Still we have to write some actions, set up some views and wire everything together. If you want to have a more detailed introduction to OpenId and DotNetOpenAuth, then you should read something like this blog entry about a quick setup for OpenId in ASP.NET MVC. The basic concept looks like that:

  • We provide a <form> with an input field and a submit button
  • The input field should contain a valid OpenId provider
  • The submission should be handled by a proper controller action (our part)
  • The controller will then send a request to the specified URL, i.e. the specified OpenId provider
  • The answer from this request will be examined and the result will have influence on our response
  • Usually the answer will be a redirect to the OpenId provider (redirect to login the user)
  • Along with that redirect we have to give the provider a valid callback URL
  • The provider's answer will be sent to the callback URL with some parameters, and here we examine the answer again
  • Finally we display the result based on the whole process

Overall we need up to three views and up to two actions. We also need to understand the OpenId API, i.e., the names and accepted values. This sounds like some work, but lucky for us we can use the mentioned DotNetOpenAuth library to do most of the work. Finally we just need one action:

C#
/* ... */
using DotNetOpenAuth.Messaging;
using DotNetOpenAuth.OpenId;
using DotNetOpenAuth.OpenId.RelyingParty;
using DotNetOpenAuth.OpenId.Extensions.SimpleRegistration;
using DotNetOpenAuth.OpenId.Extensions.AttributeExchange;

namespace SuperMario.Controllers
{
    public class AccountController : Controller
    {
        static OpenIdRelyingParty openid;

        public AccountController()
        {
            openid = new OpenIdRelyingParty();
        }

        [ValidateInput(false)]
        public ActionResult Authenticate(string openid_identifier)
        {
            var response = openid.GetResponse();

            //Distinguish between: Redirect FROM OpenId provider and TO OpenId
            if (response == null) // this case: TO OpenId
            {
                Identifier id;

                if (Identifier.TryParse(openid_identifier, out id))
                {
                    try
                    {
                        var request = openid.CreateRequest(id);
                        var fetch = new FetchRequest();
                        fetch.Attributes.AddRequired(WellKnownAttributes.Contact.Email);
                        request.AddExtension(fetch);
                        return request.RedirectingResponse.AsActionResult();
                    }
                    catch (ProtocolException ex)
                    {
                        TempData.Add("StatusMessage", ex.Message);
                        return RedirectToAction("Index", "Home");
                    }
                }
                else
                {
                    TempData.Add("StatusMessage", "Invalid identifier");
                    return RedirectToAction("Index", "Home");
                }
            }
            else // this case: FROM OpenId
            {
                switch (response.Status)
                {
                    case AuthenticationStatus.Authenticated:
                        // Create account if not already existing
                        // Login user
                        /* ... */
                        return RedirectToAction("Index", "Home");

                    case AuthenticationStatus.Canceled:
                        TempData.Add("StatusMessage", "Canceled at provider");
                        return RedirectToAction("Index", "Home");

                    case AuthenticationStatus.Failed:
                        TempData.Add("StatusMessage", response.Exception.Message);
                        return RedirectToAction("Index", "Home");
                }
            }

            return new EmptyResult();
        }
    }
}

Since only providing a textfield (with a cryptic URL to enter) and a submit field is a little bit (to say the least) user-unfriendly, we should use a list of (some) available OpenId providers. Again, this could result in too much work considering that there are already some really good free solutions out there. One of the best solutions is the OpenId selector. This one is basically a JavaScript solution (for jQuery or other popular libraries), which transforms our plain form field to a colorful, button-rich, selection. Now the user can select his favorite OpenId provider by just one simple click. All we have to do is to provide the sprite sheet (or our own compilation) for the graphics and to setup the JavaScript.

Once we changed the img_path variable inside the openid-jquery.js code, we have adjusted our login page. The final login page now additionally contains the following possibilities:

Login with OpenAuth

Integrating touch friendly buttons is quite straight forward (for any web application). All we need to do is to make any link (or clickable element in general) big enough. We do not have to follow the Metro design principles guide here - but we can use some of the tips described in it. Those principles have been compiled together in the Metro design language document.

We will create big rectangular buttons, with a colored background. Real (text) hyperlinks will only be used on a few occasions. The main menu, i.e. the view the user will see directly after the web application has loaded then looks like the following image:

The main menu after the game has loaded

The concept has to be ported to every dialog. This has some implications for designing the whole application. One important aspect lies in the visualization of dialogs. We use the full viewport to display questions to the user. The button row below the viewport is then used for the possible answers. Let's have a quick look at one example:

The modal dialog on opening the editor

In order to make the game playable on mobile devices we setup the commonly known <meta name="viewport"> directive. A full explanation is available at the Mozilla Developer Network (MDN). Our rule looks like:

XML
<meta name="viewport" content="width=640, user-scalable=no, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0">

Another adjustment for mobile devices are device (width) specific CSS rules. Using this CSS3 feature we can make a special look for devices with lower resolutions. One important feature is that the game buttons will always be visible within the game area. Usually the game buttons are positioned in the button row below the viewport. This looks like the following image:

The game including the touch buttons

On mobile devices we can use the following CSS rules:

CSS
@media only screen and (max-width: 900px) 
{
    #editGame { display: none; }
    #sections { top: 50%; margin-top: -240px; }
    #toppanel { z-index: 0; }
    #topnav { position: relative; z-index: 10; }
    #bottompanel { top: auto; bottom: 0; z-index: 10; opacity: 0.8; }
}

Most importantly the top and bottom panel (responsible for displaying the menu buttons; the top navigation was really generic, while the bottom navigation always showed actions for the current content) have been altered. Now the content screen is always in the middle of the display, which could overlap the top and bottom navigation. Therefore the top and the bottom navigation had to be brought in front of the content screen. This is done by changing the z-index rule to a value of 10.

The bottom row is also shown in the game (with buttons that can be used as touch input panels), which could (and probably will) result in overlapping with the game. To avoid restrictions in the game flow, e.g. by not seeing the character or part of the level any more, the buttons have been made transparent. Here we just set the rule for opacity to a value that is lower than 1.0 (opaque). In this case a value of 0.8 should be sufficient.

The editor dropped out on mobile devices, since the editor's interface is probably the hardest (of all interfaces in the Mario5 web application) to port to mobile devices with resolutions lower than 900px on the long edge. This can be seen by having a look at a screenshot of the editor:

The editor on a 13 inch MacBook Air in fullscreen mode

One more thing: Animations!

Another thing we need for such a platform are in-game sequences. Right now we just have one use for it: as a benefit for being victorious in the single player campaign.

Usually we want the big Mario to be apparent in such animations. We also (for that specific final scene) we need Mario's darling Peach. Those two specialized characters can be created really quick and straight forward:

JavaScript
var Peach = Hero.extend({
	init: function(x, y, level) {
		this.width = 80;
		this._super(x, y, level);
		this.setSize(46, 64);
		this.direction = directions.right;
		this.setImage(images.peach, 0, 80);
	},
	setVelocity: function(vx, vy) {
		this._super(vx, vy);
		
        if(vx !== 0) {
			if(!this.setupFrames(6, 4, false, 'Walk'))
				this.setImage(images.peach, 138, 80);
		} else if(this.frameTick) {
            this.clearFrames();
			this.setImage(images.peach, 0, 80);
		}
	},
}, 'peach');

var BigMario = Hero.extend({
	init: function(x, y, level) {
		this._super(x, y, level);
		this.direction = directions.right;
		this.setSize(32, 62);
		this.setImage(images.sprites, 0, 88);
	},
	setVelocity: function(vx, vy) {
		this._super(vx, vy);
		
        if(vx !== 0) {
			if(!this.setupFrames(9, 2, false, 'WalkRightBig'))
				this.setImage(images.sprites, 32, 88);
		} else if(this.frameTick) {
            this.clearFrames();
			this.setImage(images.sprites, 0, 88);
		}
	},
}, 'bigmario');

Note that the BigMario class could have been avoided, but those few lines are actually shorter than adjusting the usual class (Mario). Also the new characters are added automatically to the level editor - which gives level creators new possibilities. Also one remark: Both classes only contain animations for running in one specific direction (left or right). This should be extended if one thinks about more sophisticated animations.

Now that we have the corresponding characters in the game we can work on the actual Animation class. Without talking much about the code we can have one look first:

JavaScript
var Animation = Level.extend({
    init: function (id) {
        this.world = $('#' + id);
		this.setPosition(0, 0);
        this.input = [];
        this.speeches = [];
        this.currentSpeeches = [];
        this.animations = [];
        this.currentAnimations = [];
        this.cycles = 0;
        this.maxCycles = 0;
		this.reset();
    },
    load: function(level) {
        this._super(level);
        this.onend = level.onend || function() {};
        this.maxCycles = Math.ceil(level.duration / constants.interval);

        for(var i = 0; i < level.characters.length; i++) {
            var character = level.characters[i];
            var figure = new (reflection[character.name])(character.x, character.y, this);

            for(var j = 0; j < character.speeches.length; j++) {
                var speech = character.speeches[j];
                this.speeches.push({
                    figure: figure,
                    start: Math.floor(speech.start / constants.interval),
                    end: Math.floor(speech.end / constants.interval),
                    text: speech.text
                });
            }

            for(var j = 0; j < character.animations.length; j++) {
                var animation = character.animations[j];
                var obj = {
                    figure: figure,
                    start: Math.floor(animation.start / constants.interval),
                    end: Math.floor(animation.end / constants.interval)
                };

                for(var key in animation) {
                    if(obj[key] === undefined)
                        obj[key] = animation[key];
                }

                this.animations.push(obj);
            }
        }

        this.speeches.sort(function(a, b) {
            return b.start - a.start;
        });

        this.animations.sort(function(a, b) {
            return b.start - a.start;
        });
    },
    createSpeech: function(s) {
		var pos = s.figure.view.position();
        s.element = $(DIV).addClass('speech-bubble').appendTo(this.world).text(s.text).css({
			left: pos.left - 90,
			top: pos.top - s.figure.view.height() - 40
		});
    },
    removeSpeech: function(index) {
        var s = this.currentSpeeches[index];
        s.element.remove();
        this.currentSpeeches.splice(index, 1);
    },
    createAnimation: function(a) {
        if(a.x !== undefined) {
            var dx = (a.x - a.figure.x) / (a.end - a.start);
            var dy = a.figure.vy;
            a.figure.setVelocity(dx, dy);
        }

        if(a.background !== undefined) {
            a.figure.setImage(a.background.image, a.background.x, a.background.y);
        }
    },
    removeAnimation: function(index) {
        var a = this.currentAnimations[index];

        if(a.x !== undefined) {
            a.figure.setVelocity(0, a.figure.vy)
        }

        this.currentAnimations.splice(index, 1);
    },
    tick: function () {
        var i = 0, figure;

        if(this.cycles === this.maxCycles) {
            this.onend();
            this.pause();
            return;
        }

        for(i = this.currentSpeeches.length; i--; ) {
            if(this.currentSpeeches[i].end === this.cycles)
                this.removeSpeech(i);
			else if(this.currentSpeeches[i].figure.vx !== 0) {
				this.currentSpeeches[i].element.css({
					left: '+=' + this.currentSpeeches[i].figure.vx
				});
			}
        }

        for(i = this.currentAnimations.length; i--; ) {
            if(this.currentAnimations[i].end === this.cycles)
                this.removeAnimation(i);
        }

        while(this.speeches.length && this.speeches[this.speeches.length - 1].start === this.cycles) {
            var speech = this.speeches.pop();
            this.createSpeech(speech);
            this.currentSpeeches.push(speech);
        }

        while(this.animations.length && this.animations[this.animations.length - 1].start === this.cycles) {
            var animation = this.animations.pop();
            this.createAnimation(animation);
            this.currentAnimations.push(animation);
        }
		
		for(i = this.figures.length; i--; ) {
			figure = this.figures[i];
			figure.move();
			figure.playFrame();
		}
		
		for(i = this.items.length; i--; ) {
			this.items[i].playFrame();
        }

        this.cycles = this.cycles + 1;
    },
});

So this is basically another implementation of the Level class. This time we rewrote methods like tick(), just to handle animations exactly our way (this excludes any hit-detections and other stuff, that is not required for our purpose right now). If we have a close look at the code we see methods like createSpeech() and createAnimation() pop up. Those methods have been created to ensure two possible actions in our animation sequences.

  1. One of the characters giving a speech / talking
  2. One of the characters doing something like walking in one direction

How does a level for this class exactly look like? Well, not differently from a real level, i.e. we have also a level array here and some properties like width and background. Additionally we have to set up characters and assign them animations and speeches. Here is an example:

JavaScript
var endingLevel = {
    /* The start is the same as in ordinary levels */
    onend: function() { }, //This one is new - a callback if the animation has ended
    duration: 16000, //The total duration of the animation - this is when the callback is executed
    characters: [ //Our array of characters
        { // First character
            name: 'bigmario', // what is the name of the character in the reflection array ?
            x: -30, // the starting position
            y: 96,  // x and y coordinates in px
            speeches: [
                {
                    start: 7500, // start at 7.5 s
                    end: 10500, // end at 10.5 s
                    text: 'Oh Daisy!' // this text will be displayed
                }
            ],
            animations: [
                {
                    start: 2500, // start at 2.5 s
                    end: 4100, // end at 4.1 s (duration = 1.6 s)
                    x: 100 // this will be the position in the end: 100px
                },
                /* and more animations */
            ]
        },
        /* and more characters */
    ],
};

One final note to those speech bubbles. We've used the CSS class speech-bubble for the speech objects. The CSS code behind this class is the following:

CSS
.speech-bubble {
    position: absolute; padding: 20px; margin: 1em 0 3em; color: #000; background: #fdfdfd; text-align: center;
    border-radius: 100px; width: 160px; height: 25px; z-index: 100; font-size: 1.3em; border: 1px solid #ccc;
}
.speech-bubble:after {
    content: ""; display: block; position: absolute; bottom: -14px; left: 92px; width: 0;
    border-width: 15px 15px 0; border-style: solid; border-color: #fdfdfd transparent;
}

This one is tricky tricky tricky. CSS gurus found out a long time ago that the one property of the border rules is actually quite useful: they connect directly. What does that mean? Well, consider a simple square (let's say 10px x 10px). We now set a simple border with 1px on each side. Now our square is actually 12px x 12px (we use the standard CSS box-sizing now, not the IE one, i.e. the more intuitive one, which can be used via box-sizing: border-box;). This is simple. What if we set the border-top to 0px? Well we have a 12px x 11px box and the border on the left-top and right-top look a little bit smoothen. Now we increase the width of the border (but the border on the top side stays at 0px). We see triangles emerging on the top-left and top-right corners. Let's do something wired and lower the area of the box (going from 102 to simply 1). We see that this is going to be a triangle! OK: Long story short we can even set the area to zero (with 0 width a 0 height) and obtain a real triangle. Using this trick (and maybe various others) we can create a lot of possible shapes. A great page is online at CSS-Tricks.com.

Points of Interest

The OpenAuth integration is actually quite important because it decreases friction of the registration process. Some people just register for every page, but most people try to minimize their online account number by making only accounts where an account is necessary for them. One example would be the obligatory Twitter registration if one needs a Twitter API key. Even really straight forward registration forms (like the one used for the Mario5 platform) are obviously painful for most people. Therefore the OpenAuth helps a lot in animating users to try or use the web application.

You can play the full version online at mario5.florian-rappl.de.

History

  • v1.0.0 | Initial release | 01.08.2012.
  • v1.1.0 | Included animations | 02.08.2012
  • v1.1.1 | Fixed some typos | 03.08.2012

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Chief Technology Officer
Germany Germany
Florian lives in Munich, Germany. He started his programming career with Perl. After programming C/C++ for some years he discovered his favorite programming language C#. He did work at Siemens as a programmer until he decided to study Physics.

During his studies he worked as an IT consultant for various companies. After graduating with a PhD in theoretical particle Physics he is working as a senior technical consultant in the field of home automation and IoT.

Florian has been giving lectures in C#, HTML5 with CSS3 and JavaScript, software design, and other topics. He is regularly giving talks at user groups, conferences, and companies. He is actively contributing to open-source projects. Florian is the maintainer of AngleSharp, a completely managed browser engine.

Comments and Discussions

 
QuestionWhy can't you upload or comment anymore? Pin
Troy Pickett30-Aug-22 8:49
Troy Pickett30-Aug-22 8:49 
QuestionLevel save not working and request for level.js dowload option Pin
Troy Pickett29-Aug-22 7:43
Troy Pickett29-Aug-22 7:43 
GeneralMy Vote 5 Pin
Shemeemsha (ഷെമീംഷ)7-Oct-14 19:07
Shemeemsha (ഷെമീംഷ)7-Oct-14 19:07 
GeneralRe: My Vote 5 Pin
Florian Rappl7-Oct-14 20:01
professionalFlorian Rappl7-Oct-14 20:01 
GeneralMy vote of 5 Pin
Praneet Nadkar6-Oct-14 18:18
Praneet Nadkar6-Oct-14 18:18 
GeneralRe: My vote of 5 Pin
Florian Rappl6-Oct-14 19:14
professionalFlorian Rappl6-Oct-14 19:14 
QuestionMy vote of 5 Pin
thatraja11-Feb-14 21:01
professionalthatraja11-Feb-14 21:01 
AnswerRe: My vote of 5 Pin
Florian Rappl12-Feb-14 0:36
professionalFlorian Rappl12-Feb-14 0:36 
QuestionEditor download not the same Pin
Member 993870826-Mar-13 15:54
Member 993870826-Mar-13 15:54 
AnswerRe: Editor download not the same Pin
Florian Rappl27-Mar-13 1:06
professionalFlorian Rappl27-Mar-13 1:06 
GeneralMy vote of 5 Pin
MaS0uD5-Mar-13 11:13
MaS0uD5-Mar-13 11:13 
GeneralRe: My vote of 5 Pin
Florian Rappl5-Mar-13 14:16
professionalFlorian Rappl5-Mar-13 14:16 
GeneralMy vote of 5 Pin
Dr.Luiji24-Sep-12 11:20
professionalDr.Luiji24-Sep-12 11:20 
GeneralRe: My vote of 5 Pin
Florian Rappl24-Sep-12 11:34
professionalFlorian Rappl24-Sep-12 11:34 
GeneralMy vote of 5 Pin
freddyrock19-Sep-12 1:07
freddyrock19-Sep-12 1:07 
GeneralRe: My vote of 5 Pin
Florian Rappl19-Sep-12 9:47
professionalFlorian Rappl19-Sep-12 9:47 
GeneralMy vote of 5 Pin
soulprovidergr16-Sep-12 23:09
soulprovidergr16-Sep-12 23:09 
GeneralRe: My vote of 5 Pin
Florian Rappl17-Sep-12 4:47
professionalFlorian Rappl17-Sep-12 4:47 
QuestionYour Keyboard Commands Do NOT work in Mobile Phones! Pin
Bill SerGio, The Infomercial King24-Aug-12 5:05
Bill SerGio, The Infomercial King24-Aug-12 5:05 
AnswerRe: Your Keyboard Commands Do NOT work in Mobile Phones! Pin
Florian Rappl24-Aug-12 10:09
professionalFlorian Rappl24-Aug-12 10:09 
GeneralMy vote of 5 Pin
Bill SerGio, The Infomercial King24-Aug-12 5:02
Bill SerGio, The Infomercial King24-Aug-12 5:02 
GeneralRe: My vote of 5 Pin
Florian Rappl24-Aug-12 9:55
professionalFlorian Rappl24-Aug-12 9:55 
GeneralMy vote of 5 Pin
burndavid939-Aug-12 14:51
burndavid939-Aug-12 14:51 
GeneralRe: My vote of 5 Pin
Florian Rappl9-Aug-12 19:53
professionalFlorian Rappl9-Aug-12 19:53 
GeneralExcellent again! Pin
Marcelo Ricardo de Oliveira8-Aug-12 4:04
mvaMarcelo Ricardo de Oliveira8-Aug-12 4:04 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.