WP7 snippet: analyzing the hyperlink button style Animation Texture Creator

The XML content importer and Windows Phone 7

Published on Wednesday, December 22, 2010 3:44:00 PM UTC in Programming

One of the Microsoft samples for XNA development contains a simple AudioManager class to play sounds and music which I wanted to base an own implementation for a project of mine on. One of the details I needed to change was that the sample code loads a fixed set of sounds hard-coded as strings. Looking for alternatives, I thought it would be nice to use XNA's XML content importer which is part of the standard content pipeline importers, so I could easily define the sounds to load and their properties in XML. Unfortunately, using it wasn't exactly straight-forward.

Getting to know the format

The first surprise for me was how little information is available about the used XML format. The above link says things like this:

Imports XML content used for editing the values of a custom object at run time. [...] This importer is designed for scenarios like importing an XML file that describes game data at run time.

Sounds great, right? That's exactly what I want, how do I use it? Unfortunately, the documentation on standard importers does not even contain links. Even when you find the documentation on the XmlImporter, it only tells you that it's a wrapper around the IntermediateSerializer. Unlike other parts of the .NET/Silverlight documentation, it seems in this case the format and its elements are not explained in detail anywhere.

A key link on the web you will stumble upon when you're in the same situation as I was is the "Teaching a man to fish" post by Shawn Hargreaves. He suggests to write a small console application that serializes the data you want to use (or some sample data) into a file and then look at the produced output. Fair enough.

The next problem you'll run into though is that by the time Shawn had written that post, there was no .NET 4.0. When you try to write that console application, you'll receive the following, very misleading compilation error:

The type or namespace name 'Pipeline' does not exist in the namespace 'Microsoft.Xna.Framework.Content' (are you missing an assembly reference?)

Only when you look at the build output and pay attention to the warnings written there you may get a clue about what is wrong:

warning MSB3253: The referenced assembly "Microsoft.Xna.Framework.Content.Pipeline, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86" could not be resolved because it has a dependency on "Microsoft.Build.Utilities.v4.0, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" which is not in the currently targeted framework ".NETFramework,Version=v4.0,Profile=Client". Please remove references to assemblies not in the targeted framework or consider retargeting your project.

.NET 4.0 added a pared-down "client profile" of the runtime that is used as the default target for new applications. You simply have to change that in the project properties (use the ".NET 4.0 Framework" target and ignore the warning) and then your application will compile and run:

image

I have to wonder why, instead of documenting the file format once so everybody can simply look it up, people all over the world are forced to write a throw-away sample application just for this and most likely run into the same issues like I did. I hope the MSDN documentation gets updated in the future.

Anyway, the reward for this will be insight into the format you have to use for your data. For example, my sound definition class looks like this:

public class SoundDefinition
{
    public float Volume
    {
        get;
        set;
    }

    public float Pan
    {
        get;
        set;
    }

    public float Pitch
    {
        get;
        set;
    }

    public bool IsLooped
    {
        get;
        set;
    }

    public string ContentPath
    {
        get;
        set;
    }

    public string Name
    {
        get;
        set;
    }
}

My sample console application uses this class to create a list of sounds:

List<SoundDefinition> defs = new List<SoundDefinition>();

defs.Add(new SoundDefinition()
{
    ContentPath = "Content/Sounds/bla",
    Name = "BlaSound",
    IsLooped = false,
    Pan = 0.5f,
    Pitch = 0.0f,
    Volume = 0.5f
});

defs.Add(new SoundDefinition()
{
    ContentPath = "Content/Sounds/bla2",
    Name = "BlaSound2",
    IsLooped = true,
    Pan = 1.5f,
    Pitch = 0.1f,
    Volume = 0.4f
});

XmlWriterSettings settings = new XmlWriterSettings();
settings.Indent = true;

using (XmlWriter writer = XmlWriter.Create("test.xml", settings))
{                
    IntermediateSerializer.Serialize(writer, defs, null);
}

The output XML will look like this:

<?xml version="1.0" encoding="utf-8"?>
<XnaContent xmlns:Generic="System.Collections.Generic" xmlns:Audio="My.Namespace">
  <Asset Type="Generic:List[Audio:SoundDefinition]">
    <Item>
      <Volume>0.5</Volume>
      <Pan>0.5</Pan>
      <Pitch>0</Pitch>
      <IsLooped>false</IsLooped>
      <ContentPath>Content/Sounds/bla</ContentPath>
      <Name>BlaSound</Name>
    </Item>
    <Item>
      <Volume>0.4</Volume>
      <Pan>1.5</Pan>
      <Pitch>0.1</Pitch>
      <IsLooped>true</IsLooped>
      <ContentPath>Content/Sounds/bla2</ContentPath>
      <Name>BlaSound2</Name>
    </Item>
  </Asset>
</XnaContent>

Ok. From there, I thought, I can simply use that sample file as a template and import my sound definitions into my Windows Phone 7 XNA project, right? Wrong.

Cross platform problems

The attentive reader notices that in the above example, the target type to use during the deserialization is coded into the XML. In this case that would be "My.Namespace.SoundDefinition". This type needs to be accessible for the process of deserialization, which effectively means you need to add a reference to the assembly or project where you defined that type in.

The problem here is that XNA content projects are .NET projects (i.e. they target the .NET Framework 4.0). My type however was defined in a Windows Phone 7 XNA application project. That project has all kinds of references to assemblies specific to the WP7 platform, and consequently, I received a nasty compilation error when I tried to build the solution. Something like:

Building content threw FileNotFoundException: Could not load file or assembly

'System.Runtime.Serialization, Version=2.0.5.0, Culture=neutral,

PublicKeyToken=7cec85d7bea7798e' or one of its dependencies.

Hmmmpf. Ok, what now? Luckily I remembered the portability feature of Silverlight that allows you to share assemblies between Silverlight and .NET. Here is a nice blog post about the capabilities and details. I wondered if that would work here too (because I needed to share an XNA game library, not a Silverlight class library), and to my surprise it did! I only had to move the "SoundDefinition" type from my XNA game project to a separate XNA game library project. This project then could be successfully referenced from both sides – the XNA content project as well as my Windows Phone 7 game project.

image

Final steps

The properties of the XML file in the content project have to look like this:

image

In the code of the AudioManager, I then use the XML to load all the individual sounds. The noteworthy part is how the content manager is used to load a list of sound definition objects:

public static void LoadSounds()
{
    // load the sound definitions from XML
    var soundDefinitions = Instance.Game.Content.Load<List<SoundDefinition>>("Sounds");

    Instance.SoundBank = new Dictionary<string, SoundEffectInstance>();

    for (int i = 0; i < soundDefinitions.Count; i++)
    {
        SoundDefinition soundDefinition = soundDefinitions[i];

        // load each sound
        SoundEffect se = Instance.Game.Content.Load<SoundEffect>(soundDefinition.ContentPath);

        // set the sound properties
        SoundEffectInstance seInstance = se.CreateInstance();
        seInstance.Volume = soundDefinition.Volume;
        seInstance.Pan = soundDefinition.Pan;
        seInstance.Pitch = soundDefinition.Pitch;
        seInstance.IsLooped = soundDefinition.IsLooped;                

        Instance.SoundBank.Add(soundDefinition.Name, seInstance);
    }
}

Conclusion

It wasn't as simple as I thought it would be, but it was still worth it. Now I can re-use this class and change the sounds to be loaded only by editing the XML in my content project. Hopefully this post will help a few people when they face similar problems with the XML content importer and WP7.

Tags: Windows Phone 7 · XNA