While we've used the content pipeline to load specific assets like textures, sound effects, and 3D models in other tutorials, we sometimes want to load arbitrary objects in our game. For example, we might want items for purchase, weapon stats, monster data, etc. in an RPG game. We could define all of that content in C# code (a good starting point while you're first sorting things out, but not necessarily the right long-term solution). But if we could define this content in separate text files, then the game designers have options for tweaking settings without needing to know and understand C#.
Putting this data into separate files also opens up the possibility of building a level editor, where this information can be edited in a UI somewhere, separate from the actual code of the game.
Yet another reason to go down this path: it opens up the option for people to mod the game without needing access to the source code.
While this tutorial won't cover modding or a level editor, we will talk about a feature of the content pipeline to load arbitrary game objects using XML.
Detour: Why XML?
XML is often considered a rather verbose format. Even though it is nominally human-readable, its clutter of tags and elements can be somewhat obtuse to work with. Yet, XML is very flexible, and is ubiquitous. You could easily make your level editor in JavaScript or Go or C and there would be libraries for working with XML. XML provides a decent balance between human-readableness, ubiquity, and power.
Step 1: Creating a Project for Type Definitions
The XML data will, of necessity, reflect objects to be loaded in memory, which means our first task will be to define the types used by those objects. We need to make some classes.
Where do we put those classes?
You might naturally think to put them in your main game project, with all of your other class definitions. However, this won't work.
The content pipeline will be checking the validity of the XML we feed to it, so the content pipeline needs to be aware of these classes.
But the main project also has a dependency on the content pipeline project.
If we make the content pipeline have a dependency on our main project, we'd create a circular dependency, which is not a good thing.
The solution is to make an additional project in the solution that contains these data definitions.
The easiest way to do this is to click on your solution node at the top of the Solution Explorer and choose Add > New Project.
Use the Class Library template to create a new, basic class library project, and give it a name. I've simply appended .Data to the end of my project name, for simplicity, but you can do whatever you want with it.
Depending on the template you created the project from, this new project is likely to contain a Class1.cs file. You don't need that, and can delete it. (Or hijack it to turn it into the Weapon class in the next step, if you'd prefer.)
Creating the Weapon Class
The next step is to make a class definition for a Weapon class, which we'll store in our XML files, and load into our game.
By this point, you should hopefully be fairly comfortable defining a simple class, so I won't get into the details here.
Create a new file called Weapon.cs and add this code to it:
namespace MonoGameXmlContent; public class Weapon { public string Name { get; set; } public string Description { get; set; } public int Cost { get; set; } }
For what we're doing right now, we don't need more than this single class definition. In a bigger game, you may end up with dozens, hundreds, or even thousands of definitions here.
You can come back to this project and add definitions for anything you end up needing. But keep in mind that as you evolve these class definitions, it is possible to break your content. For example, if you rename Name to Title, you'll need to do some repair work on all of your existing XML files.
Build the Data Project
At this point, you'll want to build the data project so a compiled version exists on the file system for later use.
Interlinking the Projects
The trickiest step is to ensure that everything knows about the bits and pieces it needs. Here is our goal:
1. The main game project needs to know about the content. (This should already be the case.)
2. The main game project needs to know about the new data project so it can use those classes in the game itself.
3. The content pipeline project needs to know about the new data project so it can inspect and load the content.
That first step is already done automatically when you make a new MonoGame project, but we'll need to specifically configure the other two.
Adding the Data Project to the Main Game Project
To add a reference to the new data project to the main game project, right-click on the main game project in the Solution Explorer and choose Add > Project Reference.
Then find the data project in the list and check the box for it.
Once added, hit the OK button to close the reference manager window.
At this point, you should be able to check and see that there is now a project dependency on the data project:
Adding the Data Project to the Content Pipeline
The next step is to add a reference to the data project from the content pipeline, so the content pipeline knows about the class definitions in the data project.
Start by opening the MGCB editor.
Then click on the top-level Content node, and look for its properties in the bottom left of the window.
You should find a References entry that probably says "None" for now (unless you've done more with your content pipeline references before this).
Click on the right side to open the Reference Editor, then press the Add button to browse to the data project that you previously compiled.
You may need to go up a couple of directories to get out of the Content folder and over into the output of the data project.
From the root of my project (called XmlContent here), I went into the project folder XmlContent.Data, then into bin, Debug, net6.0, where XmlContent.Data.dll was at. (From the project root, the whole path is XmlContent.Data/bin/Debug/net6.0/XmlContent.Data.dll.)
I do want to point out some trickiness here. This is adding a reference to a specific DLL. You can see, from the path, that if you change your project name, your build configuration (to Release, for example), your target .NET version (say to .NET 7 or higher), this path will be broken. But importantly, there's a good bet that the old file will still be there on disk, at least until you clean it out. That means you may make a change to this data project that doesn't seem to be taking effect, because you're still pointing to an older version! It is the kind of thing that will bite you, so try to remember that this is the kind of thing that bites.
There are things you could do to reduce the chances of this pain. For example, you could add a post-build step to the data project to copy the final DLL to a specific location and then reference it from there instead. But that is a step we're not going to worry about right now.
At this point, our project references should all be working!
Adding XML Content
The next step is to add our actual XML content by defining a weapon in XML.
There are a lot of ways you could create this XML file, but a good way to see the bare minimum infrastructure is to have the MGCB Editor make it for you.
As an optional step, you might consider adding a new folder for Weapons in the MGCB Editor first, which I've done.
Then right-click on the folder and choose Add > New Item. Type in the name of the item (I've called mine Broadsword) and pick the XML Content (.xml) type from the list.
Create the file and then double-click on the file in the Project browser in the MGCB Editor to open up the newly created file using your computer's default application for .XML files. You should see text like this:
<?xml version="1.0" encoding="utf-8"?> <XnaContent xmlns:ns="Microsoft.Xna.Framework"> <Asset Type="Object"> </Asset> </XnaContent>
The <XnaContent> and <Asset> nodes are both boilerplate stuff that the XML Content processor expects to be there by default.
This tutorial is not the place to describe XML and how it works in depth, so I'm not going to get into the details of anything that you see there. However, we do want to modify this file to contain other text specific to our game.
Here is my final version:
<?xml version="1.0" encoding="utf-8" ?> <XnaContent xmlns:ns="Microsoft.Xna.Framework"> <Asset Type="MonoGameXmlContent.Weapon"> <Name>Broadsword</Name> <Description>A heavy sword meant for cutting rather than thrusting.</Description> <Cost>450</Cost> </Asset> </XnaContent>
Note that I changed the Type property to be the fully-qualified name of my Weapon class. That tells the content pipeline which type this data will represent, so it can check for validity.
Beneath that, there are other XML nodes for the properties defined in the Weapon class.
Save this version of the file, and close it out. We're done with it for now. (Until we want to tweak its parameters or add more information.)
With these updates in place, we should be able to build the content in the MGCB Editor to check that it is all working as expected.
Building the content gives us a chance to confirm that we've correctly linked our data project to the content pipeline and that our content file is well-formatted.
Loading the XML Content
The final step is to load the content into our game.
We can test this out by adding the following to our game's LoadContent method:
Weapon broadsword = Content.Load<Weapon>("Weapons/Broadsword");
Compiling and running that will prove out that it is indeed working and loading correctly.
However, this is hardly the natural ending point for this content.
You'll want to store this data elsewhere and use it throughout the game.
Using Two Representations
I'll also bring up a suggestion that I typically try to do. The "natural" storage format may not directly align with the correct representation of these things in the game as it runs. The Weapon class we added to our data project has public setters for everything. It is a simple data storage object. We may want the real representation of weapons to be more complex, and have other methods and logic to enforce. I will nearly always end up making a separation between the two. I'll have a simple data-only class with properties that are all public with both getters and setters. Then I'll load them and convert them to my game's "real" representation of weapons after I load the data. That is, my data-centric class may look like this:
public class WeaponData { public string Name { get; set; } public string Description { get; set; } public int Cost { get; set; } }
But my "real" representation of weapons might look like this:
public class Weapon { public string Name { get; } public Description { get; } public void Use(GameTime elapsedTime) { ... } public bool IsEquipped { get; set; } public int Cost { get; } public Weapon(string name, string description, int cost) { Name = name; Description = description; Cost = cost; } }
Then when I'm loading content, I'll load the data and create a "true" Weapon instance:
WeaponData broadswordData = Content.Load<WeaponData>("Weapons/Broadsword"); Weapon broadsword = new Weapon(broadswordData.Name, broadswordData.Description, broadswordData.Cost);
This allows me to evolve the data storage representation separately from the gameplay representation, which is nearly always worth it to me. Not everybody does that, so do it if you feel the desire, and skip it if you think it is unhelpful.