I took December off to spend time with family and invest in some professional development. I worked mostly with HTML5 and javascript, but I did do some Drupal work too.
For HTML5, I played with local storage, the new tags, backwards compatibility, PhoneGap, and finally the canvas tag. One thing I wanted to do was learn how to build games in HTML5. There are some great tutorials on starting a project from scratch—I’ll post more on them later, but there’s also a very good javascript game engine called ImpactJS.
ImpactJS is written almost entirely in javascript, but it does us a few php files (and a server) to allow it to do things like read from and write to files. According to the instructions, you need to set up in Apache (but an IIS port is available).
But I spend most of my life in Visual Studio 2010 (and occasionally Eclipse). I work faster in that IDE and all the keyboard shortcuts come second nature. After purchasing Impact and creating a few games, I found it tedious assigning a new port in the Apache conf on my local machine for every game. Also, the Komodo IDE is very good, but I prefer my boring ol’ Visual Studio.
So, I looked at the php files and created my own Visual Studio template. This may sound a little heavy using this big IDE only for javascript and html, but I find it very helpful that it will create a new server port each time I create a new web project.
I created a project template was going to post it online, but it includes all the ImpactJS source (which costs $99—but I got for $49 on a Christmas sale), so that probably wouldn’t be doing the developer any favours if I put it on all online. So, here are my contributions and the instructions for anyone else who has purchased ImpactJS and wants to use it in Visual Studio 2010.
Step 1: Create a New Project.
Create an empty ASP.Net Web project. Make sure to choose an empty project—otherwise you have to delete all the plumbing they give you. All you want is the web.config (and you can get rid of that if you don’t need it).
Step 2: Add ImpactJS framework
Drag all your ImpactJS source into the new project from Windows Explorer. This should include all the folders. The two root documents should be index.html and weltmeister.html.
At this point, you can right-click on index.html and choose “View In Browser”. It will give you the “It Works!” text on the canvas (but it will create a server instance like localhost:12345). This is unimpressive. The game itself does not use the server—the Weltmeister tool does.
Step 3: Add the Generic Handlers.
You can right-click and view the Weltmeister tool in the browser now, but it will not work the way it should. The Weltmeister level editor uses 4 php files:
- /lib/weltmeister/api/browse.php (browses for files in the filesystem)
- /lib/weltmeister/api/config.php (provides the $fileRoot variable and a few methods).
- /lib/weltmeister/api/glob.php (gets files matching a certain pattern in the directories)
- /lib/weltmeister/api/save.php (saves files edited in Weltmeister to the directory)
The contents of these four files are pretty straight forward and easy to convert to C#. I initially created a aspx pages to replace them, but then found Generic Handlers to be more effective since I didn’t need a front end webform.
So, we will create four files to replace these four (they can sit side by side—the php files won’t get in our way).
First add a Web.config file to the api folder (it will limit the scope to this folder only). In this file add the fileRoot variable:
<?xml version="1.0"?> <configuration> <appSettings> <add key="fileRoot" value="../../.."/> </appSettings> </configuration>
Now, add three more files of type “Generic Hander” to the api folder.
Give these files the same names as their PHP counterparts:
- browse.ashx
- glob.ashx
- save.ashx
Here is the code for each file:
browse.ashx:
using System.IO; using System.Text; using System.Web; using System.Web.Script.Serialization; namespace SpaceShooter.lib.weltmeister.api { /// <summary> /// Summary description for browse /// </summary> public class browse : IHttpHandler { public void ProcessRequest(HttpContext context) { var fileRoot = context.Request.MapPath(System.Configuration.ConfigurationManager.AppSettings["fileRoot"].ToString()); if (!fileRoot.EndsWith("/")) fileRoot += "/"; var dir = fileRoot + context.Request.QueryString["dir"].ToString(); if (!dir.EndsWith("/")) dir += "/"; var find = "*.*"; switch (context.Request.QueryString["type"].ToString()) { case "images": find = "*.{png,gif,jpg,jpeg}"; break; case "scripts": find = "*.js"; break; } var dirs = Directory.GetDirectories(dir, "*", SearchOption.AllDirectories); var files = Directory.GetFiles(dir, find, SearchOption.AllDirectories); var fileRootLength = fileRoot.Length; for (var i = 0; i < files.Length; i++) { files[i] = files[i].Replace(fileRoot, ""); } for (var i = 0; i < dirs.Length; i++) { dirs[i] = dirs[i].Replace(fileRoot, ""); } var parent = dir.Substring(0, dir.ToString().IndexOf("/")); context.Response.ContentType = "application/json"; context.Response.ContentEncoding = Encoding.UTF8; var jserializer = new JavaScriptSerializer(); context.Response.Write(jserializer.Serialize(new Response() { parent = parent, dirs = dirs, files = files })); } public bool IsReusable { get { return false; } } public class Response { public string parent { get; set; } public string[] dirs { get; set; } public string[] files { get; set; } } } }
glob.ashx:
using System.Collections.Generic; using System.IO; using System.Text; using System.Web; using System.Web.Script.Serialization; namespace SpaceShooter.lib.weltmeister.api { /// <summary> /// Summary description for glob /// </summary> public class glob : IHttpHandler { public void ProcessRequest(HttpContext context) { var fileRoot = context.Request.MapPath(System.Configuration.ConfigurationManager.AppSettings["fileRoot"].ToString()); if (!fileRoot.EndsWith("/")) fileRoot += "/"; var globs = context.Request.QueryString["glob[]"].ToString(); List<string> files = new List<string>(); //get the files foreach (var glob in globs.Split(',')) { var pattern = glob.Replace("..", "").Replace("/","\"); files.AddRange(Directory.GetFiles(fileRoot, pattern)); } //remove the fileRoot and reverse slashes for (var i = 0; i < files.Count;i++ ) { files[i] = files[i].Replace(fileRoot, ""); files[i] = files[i].Replace(@"","/"); } context.Response.ContentType = "application/json"; context.Response.ContentEncoding = Encoding.UTF8; var jserializer = new JavaScriptSerializer(); context.Response.Write(jserializer.Serialize(files)); // return ""; } public bool IsReusable { get { return false; } } } }
save.ashx:
using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Web; using System.Web.Script.Serialization; namespace SpaceShooter.lib.weltmeister.api { /// <summary> /// Summary description for save /// </summary> public class save : IHttpHandler { public void ProcessRequest(HttpContext context) { var fileRoot = context.Request.MapPath(System.Configuration.ConfigurationManager.AppSettings["fileRoot"].ToString()); if (!fileRoot.EndsWith("/")) fileRoot += "/"; var path = context.Request.Form["path"].ToString(); var data = context.Request.Form["data"].ToString(); var result = new Result(); if (!string.IsNullOrEmpty(path) && !string.IsNullOrEmpty(path)) { path = fileRoot + path.ToString().Replace("..", ""); if (path.EndsWith(".js")) { try { //if the file already exists, delete it if (File.Exists(path)) { File.Delete(path); } var streamWriter = File.CreateText(path); streamWriter.Write(data); } catch (Exception ex) { result.error = "2"; result.msg = string.Format("Couldn't write to file: {0}", path); } } else { result.error = "3"; result.msg = "File must have a .js suffix"; } } else { result.error = "1"; result.msg = "No Data or Path specified"; } context.Response.ContentType = "application/json"; context.Response.ContentEncoding = Encoding.UTF8; var jserializer = new JavaScriptSerializer(); context.Response.Write(jserializer.Serialize(result)); } public bool IsReusable { get { return false; } } public class Result { public string error { get; set; } public string msg { get; set; } } } }
Your final api folder should look like this:
You can get rid of the php files if you wish, but they should not be interfering with anything.
Step 4: Amend config.js
Finally, you need to tell the javascript classes to run these ashx files instead of the php files, this is can in the file /lib/welbmeister/config.js under the api property. Simply change the php extension to ashx:
'api': { 'save': 'lib/weltmeister/api/save.ashx', 'browse': 'lib/weltmeister/api/browse.ashx', 'glob': 'lib/weltmeister/api/glob.ashx' }
Step 5: Save the project as a template
Now, you don’t want to have to do this for every new game project, so you need to turn this into a template (but you might want to test out the Weltmeister tool first and make sure everything is running as it should).
To create a new template, simply go to the file menu of Visual Studio and choose “Export Template”. Then you can go through the wizard to create a new template on your pc to quickly create new ImpactJS game projects.
You may also want to create an “entity” template to speed things up when you create new entities for your game using the same menu (but choose Item Template instead of Project Template).
What about Baking?
Okay, there are two other php files in the framework that could be converted. When you finish your ImpactJS game, you can use a command line tool to “bake” the game and get it ready for deployment. This basically consolidates all the javascript files into one file and minifies it. This is done in the tools folder using a file called bake.php.
I have not converted the bake file to c# yet. I don’t deploy too often. However, I may convert it in the future. I would use the aspx port of jsmin written by Laurent Bugnion and convert bake.php to an aspx or ashx file. The ImpactJS developer has put very little reliance on php, so conversion should not be difficult.
Let me know how it goes
That’s it. I hope, if you found this post, this gets you building games in Visual Studio (with your familiar shortcuts, plug ins, and code-completion). If you found it useful, please add a comment below. And if you convert the baking file, please let me know too.
garmin 1490t says
Such a well written post.. Thnkx for sharing this post!
Robo says
Hmm
this line in the glob.ashx doesn’t compile:
var pattern = glob.Replace(“..”, “”).Replace(“/”,””);
I tried doing: .Replace(“/”,@””); but then the code fails in other parts. Would you be able to upload your template somewhere?
Robo says
I had a couple of problems with your code and was hoping you could help clear it up.
First, within the glob.ashx.cs file we have line 26
var pattern = glob.Replace(“..”, “”).Replace(“/”,””);
Unfortunately, this line doesn’t complile.
I fixed it with: var pattern = glob.Replace(“..”, “”).Replace(“/”,@””);
From there the editor seems to work properly, but it fails (and seems to become non-responsive) after trying to select a tileset for a new layer.
I’m not sure what’s going on but I think the line
var files = Directory.GetFiles(dir, find, SearchOption.AllDirectories);
Should return an array of filenames but when I debug it, it comes up empty.
wroolie says
Robo,
You are completely right. I just went through the template again and found a few issues. First, the glob.ashx was not replacing the right character. I should read:
//remove the fileRoot and reverse slashes
for (var i = 0; i < files.Count; i++)
{
files[i] = files[i].Replace(fileRoot, "");
files[i] = files[i].Replace(@"", "/");
}
Second, the browse.ashx was not getting the images properly. Here is the fixed code for the browse.ashx Process method:
var fileRoot = context.Request.MapPath(System.Configuration.ConfigurationManager.AppSettings["fileRoot"].ToString());
if (!fileRoot.EndsWith("/"))
fileRoot += "/";
var dir = fileRoot + context.Request.QueryString["dir"].ToString();
if (!dir.EndsWith("/"))
dir += "/";
var find = "*.*";
var type = context.Request.QueryString["type"].ToString();
switch (context.Request.QueryString["type"].ToString())
{
case "images":
find = "*.{png,gif,jpg,jpeg}";
break;
case "scripts":
find = "*.js";
break;
}
var dirs = Directory.GetDirectories(dir, "*", SearchOption.AllDirectories);
var filesList = new List();
if (type == “images”)
{
filesList = Directory
.GetFiles(dir, “*.*”,SearchOption.AllDirectories)
.Where(file => file.EndsWith(“png”) || file.EndsWith(“gif”)
|| file.EndsWith(“jpg”)
|| file.EndsWith(“jpeg”)).ToList();
}
else
{
filesList = Directory.GetFiles(dir, find, SearchOption.AllDirectories).ToList();
}
var fileRootLength = fileRoot.Length;
for (var i = 0; i < filesList.Count; i++)
{
filesList[i] = filesList[i].Replace(fileRoot, "");
}
for (var i = 0; i < dirs.Length; i++)
{
dirs[i] = dirs[i].Replace(fileRoot, "");
}
var parent = dir.Substring(0, dir.ToString().IndexOf("/"));
context.Response.ContentType = "application/json";
context.Response.ContentEncoding = Encoding.UTF8;
var jserializer = new JavaScriptSerializer();
context.Response.Write(jserializer.Serialize(new Response()
{
parent = parent,
dirs = dirs,
files = filesList.ToArray()
}));
I'll amend this in the blog post.
Thank you very much for pointing this out.
Eric