In this previous post, we already showed how to use T4 templates for several use cases, with a special focus on Resource Files (ResX). Now we are going to complete that post with another one, focused specifically on Content management.
What we want to achieve is an elegant, cross-platform, and strongly-typed way of accessing contents in our projects.
The XNA Approach to Contents
XNA identifies contents with Asset Names, in the form of string
s, but it doesn’t offer any form of strong-typed access, what is very bug-prone, because if you misspell the name of an asset, you won’t notice until runtime, or you won’t notice ever…
The Android Approach to Contents
Android already offers strongly-typed access to contents that are placed below the “Resources” special folder. Unfortunately, there are a lot of limitations for the contents inside that folder. One of the most evident (and stupid) ones is that contents cannot be re-arranged into subfolders, what makes it almost un-usable for medium-big projects. Besides that, the kind of access Android gives to that folder is through INT identifiers, what conflicts with the XNA way of doing this (which uses Asset names).
One of the possible solutions is to move our contents to the “Assets” folder, where things can be arranged arbitrarily, and where assets are identified with a string
, very much like in XNA. Too bad that Android doesn’t offer strongly-typed access to that folder…
What We Want to Achieve
- We want to be able to arrange our contents in sub-folders, so in Android, we will have to go to the Assets approach, instead of the Resources one.
- That solves also the unification of types when identifying assets. In both cases (XNA and Android), we will be using Asset Names as
string
s. - In both sides, we will need to provide a strongly-typed way of accessing contents.
- We want the exact same interface that is finally published outwards, so that every piece of code that uses our strongly-typed classes, writes the exact same code no matter which platform we are coding on.
An Implementation Using Again T4 Templates
Again, we will write down two different T4 templates, one for XNA and one for Android. Both of them will have to do merely the same, but with some minor differences. Let’s see them:
Example: XNA T4 Template to Give Strongly-typed Access to Contents
This template will be placed wherever we want to use it. It can be in the main XNA Game project, or in a library project shared all around. Basically, it will search inside the Visual Studio solution for the Game’s Content Project. Once found, it will iterate recursively through the file and folder structure of the project, generating classes that give strong-typed access to each Asset.
Imagine we have the following structure in our contents
project:
We want to get an output like the following:
As you can see, we will use namespace
s to represent the tree-structure of folders in the content project. Once we find a folder with one or more content files, we will create a class named “Keys
” that will hold properties to access asset names. We will also create an enumeration with all the assets found at that level. This way, we also allow to navigate through the contents
tree, if needed.
The Code
The XNA template which generates that is the following:
<#
#>
<#@ template debug="true" hostspecific="true" #>
<#@ assembly name="System.Core" #>
<#@ assembly name="System.Xml" #>
<#@ assembly name="Microsoft.VisualStudio.Shell.Interop.8.0" #>
<#@ assembly name="EnvDTE" #>
<#@ assembly name="EnvDTE80" #>
<#@ assembly name="VSLangProj" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Text.RegularExpressions" #>
<#@ import namespace="System.Xml" #>
<#@ import namespace="Microsoft.VisualStudio.Shell.Interop" #>
<#@ import namespace="EnvDTE" #>
<#@ import namespace="EnvDTE80" #>
<#@ import namespace="Microsoft.VisualStudio.TextTemplating" #>
<#
var serviceProvider = Host as IServiceProvider;
if (serviceProvider != null)
Dte = serviceProvider.GetService(typeof(SDTE)) as DTE;
if (Dte == null)
throw new Exception("T4MVC can only execute through the Visual Studio host");
Project = GetXNAContentsProject(Dte);
if (Project == null)
{
Error("Could not find XNA Content Project.");
return"XX";
}
Project prjT4 = GetProjectContainingT4File(Dte);
if (prjT4 == null)
{
Error("Could not find Template's project");
return"XX";
}
AppRoot = Path.GetDirectoryName(Project.FullName) + '\\';
RootNamespace = prjT4.Properties.Item("RootNamespace").Value.ToString();
#>
using System.Threading;
<#
try
{
AllEntries = new Dictionary<string, List<AssetFileInfo>>();
string projectFileName = Path.GetFileName(Project.FullName);
string projectFullPath = Project.FullName.Substring
(0, Project.FullName.Length - projectFileName.Length);
FindResourceFilesRecursivly(projectFullPath, Project.ProjectItems, "");
foreach(string path in AllEntries.Keys)
{
if(path == null || path == "")
continue;
List<string> enumNames = new List<string>();
string aux = path;
string className = "Keys";
if(aux.EndsWith("\\"))
aux = aux.Remove(aux.Length - 1, 1);
string pathNameSpace = aux.Replace("\\", ".");
if(pathNameSpace != "")
WriteLine(string.Format("namespace {0}.Assets.{1}",
RootNamespace, pathNameSpace));
else WriteLine(string.Format("namespace {0}.Assets", RootNamespace));
WriteLine("{");
WriteLine(string.Format("\tpublic class {0}", className));
WriteLine("\t{");
foreach(AssetFileInfo info in AllEntries[path])
{
string filenameWithoutExt= Path.GetFileNameWithoutExtension(info.File);
WriteLine(string.Format
("\t\tpublic static string {0}", filenameWithoutExt));
WriteLine("\t\t{");
WriteLine(string.Format("\t\t\tget {{ return \"{0}\"; }}",
info.AssetName.Replace(@"\", @"\\") ));
WriteLine("\t\t}");
enumNames.Add(filenameWithoutExt);
}
WriteLine("\t\tpublic enum eKeys");
WriteLine("\t\t{");
foreach(string enumname in enumNames)
WriteLine(string.Format("\t\t\t{0},", enumname));
WriteLine("\t\t}");
WriteLine("\t}");
WriteLine("}");
}
}
catch(Exception ex)
{
Error(ex.ToString());
}
#>
<#+
const string Kind_PhysicalFolder = "{6BB5F8EF-4483-11D3-8BCF-00C04F8EC28C}";
bool AlwaysKeepTemplateDirty = true;
static DTE Dte;
static Project Project;
static string AppRoot;
static string RootNamespace;
static Dictionary<string, List<AssetFileInfo>> AllEntries;
static List<string> SupportedExtensions = new List<string>()
{".dds", ".png", ".bmp", ".tga", ".jpg"};
void FindResourceFilesRecursivly(string pProjectFullPath,
ProjectItems items, string path)
{
string assetRelativePath = path.TrimStart(new char[1]{'.'});
assetRelativePath = assetRelativePath.Replace('.', '\\');
foreach(ProjectItem item in items)
{
if(item.Kind == Kind_PhysicalFolder)
FindResourceFilesRecursivly(pProjectFullPath,
item.ProjectItems, path+"."+item.Name);
else
{
string extension = Path.GetExtension(item.Name).ToLowerInvariant();
if(SupportedExtensions.Contains(extension))
{
string itemFileName = item.FileNames[0];
if(itemFileName == null)
continue;
AssetFileInfo info = new AssetFileInfo();
info.AssetName = itemFileName.Remove(0, pProjectFullPath.Length);
info.AssetName = info.AssetName.Substring
(0, info.AssetName.Length - extension.Length);
info.File = item.Name;
info.Path = itemFileName.Substring(0,
itemFileName.Length - item.Name.Length);
if(!AllEntries.ContainsKey(assetRelativePath))
AllEntries.Add(assetRelativePath, new List<AssetFileInfo>());
AllEntries[assetRelativePath].Add(info);
}
}
}
}
Project GetXNAContentsProject(DTE dte)
{
foreach(Project prj in dte.Solution.Projects)
{
if(!HasProperty(prj.Properties,
"Microsoft.Xna.GameStudio.ContentProject.
ContentRootDirectoryExtender.ContentRootDirectory"))
continue;
return prj;
}
return null;
}
Project GetProjectContainingT4File(DTE dte)
{
ProjectItem projectItem = dte.Solution.FindProjectItem(Host.TemplateFile);
if (projectItem.Document == null)
projectItem.Open(Constants.vsViewKindCode);
if (AlwaysKeepTemplateDirty) {
projectItem.Document.Saved = false;
}
return projectItem.ContainingProject;
}
private bool HasProperty(Properties properties, string propertyName)
{
if (properties != null)
{
foreach (Property item in properties)
{
if (item != null && item.Name == propertyName)
return true;
}
}
return false;
}
struct AssetFileInfo
{
public string AssetName {get;set;}
public string Path { get; set; }
public string File { get; set; }
}
#>
Usage
Once the T4 templates is included on your solution, the way of accessing Assets in XNA is like the following:
Content.Load<Texture2D>(GDNA.PencilBurst.Assets.Textures.UI.Keys.circleT);
All absolutely strong-typed, much less bug-prone.
Android Version
Once you have that as a start point, developing the Android version is pretty straight-forward. You just need to change a couple of things:
And that's all !!!