Tuesday, July 13, 2010

Abilities (Extending the XNA Content Pipeline)

Over the past few weeks I have been learning how to extend the XNA Content Pipeline.

XNA has a central component to handle importing content like fonts, 3d models, images, audio etc. This is called the Content Pipeline. When we want to define a new type of content, for example a map file or an ability file we need to extend the pipeline.

Before I go into the explanation of how to do this, let me explain some of the pros and cons that I have found. This is probably not definitive, and definitely subjective. One of the reasons I would rather bring in an ability through the pipeline rather than any other way is that content gets processed at compile time rather than during runtime. When the game is compiled the original file gets processed into a proprietary file that is more difficult to hack/edit. This helps stop people from editing an ability file to make it do more damage, or edit a character class to give it abilities it shouldn't have. One of my favorite pros is that you can add all your content files right into the content folder in Visual Studio and organize them in folders. This makes it easier to maintain things for me. The main con is that it can sometimes be a lot of work to learn how to extend the pipeline and you need to extend it every time you add a new content type. Furthermore you need to edit a few files if you decide to make changes to what is held in a file (like if you decide that now an ability file also needs to hold another bit of info you didn't think of before.)

So what does it take to extend the pipeline? Lets walk through the files I made for holding and processing ability information.

XML Example - WolfsHowl.abil
<Ability>
  <Name>Wolf's Howl</Name>
  <Description>
    Frightens Nearby Enemies.
  </Description>
  <Type>Attack</Type>
  <Cost MP="5" HP="0" />
  <Effects>
    <Effect Attribute="Def" Modifier="-2" />
  </Effects>
</Ability>

Here is the sample ability "Wolf's Howl". As you can see it holds information like the name, description, cost, and effects of the ability. In the future I want to expand this to hold the "shape" and range of an ability and other things, but this is a sample.

This xml file is saved as an ".abil" file. Therefore in my trpgProcessors project I need to define an importer that tells the project how .abil files should be handled.

AbilityImporter.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content.Pipeline;
using Microsoft.Xna.Framework.Content.Pipeline.Graphics;

// TODO: replace this with the type you want to import.
using TImport = System.Xml.XmlDocument;
using System.Xml;

namespace trpgProcessors.Ability
{
    /// <summary>
    /// This class will be instantiated by the XNA Framework Content Pipeline
    /// to import a file from disk into the specified type, TImport.
    ///
    /// This should be part of a Content Pipeline Extension Library project.
    /// </summary>
    [ContentImporter(".abil", DisplayName = "Ability Importer", DefaultProcessor = "AbilityProcessor")]
    public class AbilityImporter : ContentImporter<TImport>
    {
        public override TImport Import(string filename, ContentImporterContext context)
        {
            XmlDocument xmlDoc = new XmlDocument();
            xmlDoc.Load(filename);
            return xmlDoc;
        }
    }
}

This file just tells the content pipeline that .abil files are xml docs that get processed by "AbilityProcessor", shown below

AbilityProcessor.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content.Pipeline;
using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
using Microsoft.Xna.Framework.Content.Pipeline.Processors;

// TODO: replace these with the processor input and output types.
using TInput = System.Xml.XmlDocument;
using TOutput = trpgProcessors.Ability.AbilityContent;
using System.Xml;
using trpgData;

namespace trpgProcessors.Ability
{
    /// <summary>
    /// This class will be instantiated by the XNA Framework Content Pipeline
    /// to apply custom processing to content data, converting an object of
    /// type TInput to TOutput. The input and output types may be the same if
    /// the processor wishes to alter data without changing its type.
    ///
    /// This should be part of a Content Pipeline Extension Library project.
    ///
    /// TODO: change the ContentProcessor attribute to specify the correct
    /// display name for this processor.
    /// </summary>
    [ContentProcessor(DisplayName = "trpgProcessors.Ability.AbilityProcessor")]
    public class AbilityProcessor : ContentProcessor<TInput, TOutput>
    {
        public override TOutput Process(TInput input, ContentProcessorContext context)
        {
            AbilityContent abilityContent = new AbilityContent();
            List<AbilityEffect> effects = new List<AbilityEffect>();
            AbilityEffect ae;
           
            foreach (XmlNode node in input.DocumentElement.ChildNodes)
            {
                if (node.Name == "Name")
                {
                    abilityContent.Name = node.InnerText;
                }
                if (node.Name == "Description")
                {
                    abilityContent.Description = node.InnerText;
                }
                if (node.Name == "Type")
                {
                    abilityContent.Type = node.InnerText;
                }
                if (node.Name == "Cost")
                {
                    abilityContent.CostMP = Convert.ToInt32(node.Attributes["MP"].Value);
                    abilityContent.CostHP = Convert.ToInt32(node.Attributes["HP"].Value);
                }
                if (node.Name == "Effects")
                {
                    foreach (XmlNode tNode in node.ChildNodes)
                    {
                        if (tNode.Name == "Effect")
                        {
                            ae = new AbilityEffect();
                            ae.Attribute = tNode.Attributes["Attribute"].Value;
                            ae.Modifier = Convert.ToDouble(tNode.Attributes["Modifier"].Value);
                            effects.Add(ae);
                        }
                    }
                    abilityContent.AbilityEffects = effects;
                }
            }
            return abilityContent;
        }
    }
}

This function goes through the xml file examining the nodes and processing the information into an AbilityContent file.

AbilityContent.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using trpgData;

namespace trpgProcessors.Ability
{
    public class AbilityContent
    {
        public string Name;
        public string Description;
        public string Type;
        public int CostMP;
        public int CostHP;
        public List<AbilityEffect> AbilityEffects;
    }
}

This while then gets sent to the AbilityWriter which writes the information out into an xnb file.

AbilityWriter.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content.Pipeline;
using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
using Microsoft.Xna.Framework.Content.Pipeline.Processors;
using Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler;

// TODO: replace this with the type you want to write out.
using TWrite = trpgProcessors.Ability.AbilityContent;
using trpgData;

namespace trpgProcessors.Ability
{
    /// <summary>
    /// This class will be instantiated by the XNA Framework Content Pipeline
    /// to write the specified data type into binary .xnb format.
    ///
    /// This should be part of a Content Pipeline Extension Library project.
    /// </summary>
    [ContentTypeWriter]
    public class AbilityWriter : ContentTypeWriter<TWrite>
    {
        protected override void Write(ContentWriter output, TWrite value)
        {
            output.Write(value.Name);
            output.Write(value.Description);
            output.Write(value.Type);
            output.Write(value.CostMP);
            output.Write(value.CostHP);
            output.Write(value.AbilityEffects.Count);

            foreach (AbilityEffect effect in value.AbilityEffects)
            {
                output.Write(effect.Attribute);
                output.Write(effect.Modifier);
            }
        }
        public override string GetRuntimeReader(TargetPlatform targetPlatform)
        {
            return typeof(trpgData.AbilityReader).AssemblyQualifiedName;
        }
    }
}

All the above processing happens when the game is compiled.


Now let's switch our attention to what happens at runtime when the game goes to load the actual content that was processed and created at compile time.

Ability.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework.Content;

namespace trpgData
{
    public class Ability
    {
        //Info
        public string Name { set; get; }
        public string Description { set; get; }

        //Can be: Attack, Buff, Util
        public string Type { set; get; }

        //Cost
        public int CostMP { set; get; }
        public int CostHP { set; get; }

        //Attack shape


        //Attack effects
        public List<AbilityEffect> AbilityEffects = new List<AbilityEffect>();


        public Ability()
        {
        }
    }

    public class AbilityEffect
    {
        public string Attribute { set; get; }
        public double Modifier { set; get; }

        public AbilityEffect()
        {
        }
    }

    public class AbilityReader : ContentTypeReader<Ability>
    {
        protected override Ability Read(
            ContentReader input, Ability existingInstance)
        {
            Ability m = new Ability();
            m.Name = input.ReadString();
            m.Description = input.ReadString();
            m.Type = input.ReadString();

            m.CostMP = input.ReadInt32();
            m.CostHP = input.ReadInt32();

            int effectCount = input.ReadInt32();

            List<AbilityEffect> effects = new List<AbilityEffect>();
            AbilityEffect ae;

            for (int i = 0; i < effectCount; i++)
            {
                ae = new AbilityEffect();
                ae.Attribute = input.ReadString();
                ae.Modifier = input.ReadDouble();

                effects.Add(ae);
            }
            m.AbilityEffects = effects;

            return m;

        }
    }
}

As you can see this file contains 3 classes. The first class is our ability class. The second is our AbilityEffect class that we use to make lists of effects. The last is the important one, the AbilityReader. This class goes through the xnb file and populates our Ability.


All of this stuff might seem like a lot but when we load the actual abilities into the game the actual code just looks like this:
Ability WolfHowl = Content.Load<Ability>(@"Abilities\\WolfHowl");

Sorry if this post was really code heavy, but code is what has taken up a lot of my time lately making all these files for both maps and abilities. Next I will be doing the same for Unit and CharacterClasses.

My next post will be focused more on design than code, so stay tuned.

No comments:

Post a Comment