© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2022
L. Salin, R. MorrarGame Development with MonoGamehttps://doi.org/10.1007/978-1-4842-7771-3_2

2. The Content Pipeline Tool

Louis Salin1   and Rami Morrar2
(1)
Cedar Park, TX, USA
(2)
San Leandro, CA, USA
 

Sometimes improving a game concept means improving some of the structural components of the game, which may not yield any visual improvements. For example, if you want artists to contribute to the game and create 2D animations to embellish the game, it would save everyone some time if the artists could define each animation frame’s position and dimensions on the sprite sheet. As it is right now, that information is defined in the code, so modifying a sprite sheet may require code changes. If you moved the animation details into the Content Pipeline Tool, artists would then be able to change animations without changing any code.

Note

As in Chapter 1, you will keep improving the game developed in the MonoGame Mastery book.

In this chapter, you will learn
  • How to extend the Content Pipeline Tool to handle custom assets

  • How to migrating the player’s animation into an asset managed by the Pipeline Tool

  • How to add text translations to the game

The Content Pipeline Tool

First, let’s start with a quick recap of what the Content Pipeline tool does for us. Its main utility to MonoGame developers is preprocessing assets so they are ready to be used in a game. It takes assets produced by graphic artists, musicians, level designers, and even programmers and transforms them so they can be used by the game code without needing to be processed again, even as the code changes and is recompiled. Once transformed, an asset does not need to be transformed again and is saved to disk in its new, transformed state as an .xnb file. As a bonus, it is also compressed to take up less space.

The content pipeline performs its work in four individual steps. It first calls an importer object that will load the asset from disk into memory. Then a processor is invoked, which transforms the data in memory into a data structure that is usable by the game. Finally, because you do not want to reprocess the asset when the game runs, the pipeline will store the data structure back to disk as an .xnb file using a writer object. When the game is launched and the asset is needed, a reader object will be called internally by MonoGame to load the data structure as-is so it can be used immediately without further transformations. Figure 2-1 shows this process for a .png image asset, which can be transformed into a Texture2D object by the pipeline.
../images/508651_1_En_2_Chapter/508651_1_En_2_Fig1_HTML.png
Figure 2-1

How the Content Pipeline Tool works

There are two ways to kick off the Content Pipeline Tool. One is from within its graphical user interface. The other is from Visual Studio when the project is built. When creating new MonoGame projects using the available Visual Studio templates, a Content.mgcb file will be generated and included in the project in the Content directory of the project. When right-clicking on that file and selecting its properties, you can see what is shown in Figure 2-2. Content.mgcb has a custom build action called MonoGameContentReference which will invoke the Content Pipeline Tool to process all of the assets as part of compiling the code. That build action is defined in the installed MonoGame NuGet package, located in the .nuget folder in your home directory. For example, mine is in C:UsersLouisSalin.nugetpackagesmonogame.content.builder.task3.8.0.1641uildMonoGame.Content.Builder.Task.targets.
../images/508651_1_En_2_Chapter/508651_1_En_2_Fig2_HTML.png
Figure 2-2

The Content.mgcb build action

Let’s look at the four types of objects needed by the content pipeline individually and look at how an image is imported and processed into a Texture2D object by the pipeline.

The Content Importer

The Content Importer is a class that is used to load an asset from disk. The class must inherit from ContentImporter<T> and be marked by a ContentImporter attribute. This will allow the Content Pipeline Tool to discover the importer and present it as an option in the Importer drop-down. Figure 2-3 shows the available importers in the Content Pipeline Tool, with the Texture Importer selected.
../images/508651_1_En_2_Chapter/508651_1_En_2_Fig3_HTML.png
Figure 2-3

The list of importers available in the Content Pipeline Tool

Within the MonoGame framework, the Texture Importer is defined like this:
[ContentImporter(
 ".bmp", // Bitmap Image File
 // ... there are a lot of image file types defined, most of which
 // ... was edited out for reading purposes
 ".jng", ".jpg", ".jpeg", ".jpe", ".jif", ".jfif", ".jfi", // JPEG
 ".png", //Portable Network Graphics
 DisplayName = "Texture Importer - MonoGame",
 DefaultProcessor = "TextureProcessor")]
public class TextureImporter : ContentImporter<TextureContent>

The code above declares a TextureImporter class that inherits from the ContentImporter with a generic type TextureContent that is a class type that will be used to hold the image data in memory once the asset is loaded. The TextureImporter is marked with the ContentImporter class attribute that defines which file types can be imported by this code, which display name to put in the drop-down selection of the content pipeline tool, and which content processor class will be needed to process the asset once loaded.

The importer’s job is simple: open up a file stream, read the content of the file, and store the data into an instance of an object designed to hold that data. This is done within the Import() function of the importer. Once loaded, the content pipeline takes the data and passes it to the next stage of the pipeline, where it will be processed.

You will keep looking at how the content pipeline handles textures in this section, so it is important to note that the TextureImporter actually creates an instance of a Texture2DContent when loading an image, which is fine because Texture2DContent inherits from TextureContent, used as the generic type of the importer. This will be important to remember later.

The Content Processor

Similarly to the importer above, the content processor is also selectable from a drop-down in the Content Pipeline Tool, as Figure 2-4 shows. The processors available to the Pipeline Tool must inherit from the ContentProcessor<TInput, TOutput> abstract class and be marked by a ContentProcessor attribute that is used to specify the display name of the processor in the drop-down. The idea behind the processor is to take an object of type TInput and transform it into an object of type TOutput before passing it onto the next stage of the pipeline.

Let’s keep looking at how your images are processed into textures. The TextureProcessor is defined like this:
[ContentProcessor(DisplayName="Texture - MonoGame")]
public class TextureProcessor : ContentProcessor<TextureContent, TextureContent>

Here, the TextureProcessor class is designed to transform an object of type TextureContent into a new object of the same type. Since the importer you looked at above creates Texture2DContent objects, which inherit from TextureContent, the output of the TextureProcessor is also a Texture2DContent instance. One of the things that is done by the processor is the generation of mipmaps, if they are needed.

Content processors can have parameters that are displayed under the Processor Parameters section of the Content Pipeline Tool’s properties. Those parameters are read from the content processor class. Any class property in the content processor class with a public getter and setter will be shown by the Content Pipeline Tool and the user will be able to change their values. Figure 2-4 happens to show the processor parameters made available by the TextureProcessor:
  • ColorKeyColor

  • ColorKeyEnabled

  • GenerateMipmaps

  • PremultiplyAlpha

  • MakeSquare

  • ResizeToPowerOfTwo

  • TextureFormat

../images/508651_1_En_2_Chapter/508651_1_En_2_Fig4_HTML.png
Figure 2-4

The list of processors available in the Content Pipeline Tool and available processor parameters

The processor parameters can be marked with a DefaultValueAttribute if you desire to change their default values, which is based on the type of the parameter.

Let’s look at how the TextureProcessor defined all these parameters:
[DefaultValueAttribute(typeof(Color), "255,0,255,255")]
public virtual Color ColorKeyColor { get; set; }
[DefaultValueAttribute(true)]
public virtual bool ColorKeyEnabled { get; set; }
public virtual bool GenerateMipmaps { get; set; }
[DefaultValueAttribute(true)]
public virtual bool PremultiplyAlpha { get; set; }
public virtual bool ResizeToPowerOfTwo { get; set; }
public virtual bool MakeSquare { get; set; }
public virtual TextureProcessorOutputFormat TextureFormat { get; set; }

Now that you have imported and processed your textures, it is time to save them back to disk.

The Content Writer

Saving a processed asset back to disk in an .xnb format is the job of the content writer. Content writers are discovered at runtime by the content pipeline tool when the time comes to save processed data of a certain type, so in order to save data of a certain type, T, there must be a class in the codebase that inherits from ContentTypeWriter<T> and is marked by the ContentTypeWriter attribute. Without that, no writer class will be found and the Pipeline Tool will fail to generate an .xnb file.

Let’s take a look at how the pipeline saves instances of the Texture2DContent class to an .xnb file. Within the framework, there is a class defined that satisfies your conditions for a valid content writer:
[ContentTypeWriter]
class Texture2DWriter : BuiltInContentWriter<Texture2DContent>

Note that BuiltInContentWriter<T> inherits from ContentTypeWriter<T>, so this works. The BuiltInContentWriter class is private and is only used for the content types that are built into the framework.

The Write() method of a given content writer will be called by the pipeline tool with two parameters: an output object with an interface allowing its user to write many different kinds of values to disk as binary data, and the content object that needs to be saved.

One secondary aspect of the content writer is that it is also responsible for indicating which content reader will be needed to load the .xnb file back into memory. Once the content object has been serialized, the Flush() method is called on the output object by the pipeline. This will cause a header to be written near the start of the .xnb file, and one of the elements in the header is the full class name of the content reader that will be instantiated to load the data back from disk. The name is accessed by calling the GetRuntimeReader() function on the content reader. You will not see one on the Texture2DWriter because it is overridden in its BuiltInContentWriter base class, where the content reader class name is set to the same name as the content writer (Texture2DWriter here), except that “Writer” is replaced with “Reader.” Using that logic, loading a Texture2DContent object will require an instance of the Texture2DReader, which we will discuss next.

The Content Reader

The content reader is a class that must inherit from ContentTypeReader<T>. Contrary to the other three classes used by the Content Pipeline Tool, this class does not need to be marked by an attribute. This is because this class is not auto-discovered by the MonoGame framework. Instead, when loading an .xnb file from disk, one of the file headers has the full class name that needs to be used to load the file. Once the class name has been read from the file headers, MonoGame will create an instance of the class and call its Read() function, which will load the same binary data that was written to disk by the content writer. This is all done when the content manager’s Load<T>() function is called to load an asset into the game.

Here is how the Texture2DReader is defined in the framework:
internal class Texture2DReader : ContentTypeReader<Texture2D>
You have finally come full circle. The Texture2DReader inherits from ContentTypeReader of type Texture2D, which is exactly what you want when call the LoadTexture in your game engine:
protected Texture2D LoadTexture(string textureName)
{
 return _contentManager.Load<Texture2D>(textureName);
}
To recap, an image is processed into a texture by going through the following steps:
  • A .png file (or other supported file format) is loaded by the TextureImporter, which creates a Texture2DContent object.

  • The Texture2DContent object is processed and enriched by the TextureProcessor.

  • The enriched Texture2DContent object is then saved to an .xnb file by the Texture2DWriter, where one of the .xnb file headers is set to the full class name of the Texture2DReader class so it can be read from disk.

  • Finally, the MonoGame reads an .xnb file, determines that a Texture2DReader instance is required, and uses it to read the file into a Texture2D object for the game.

We covered quite a bit of internal details here, but this knowledge will be important should you want to develop your own importers, processors, writers, and readers. Extending the content pipeline tool to be able to handle custom data types is not too unusual and it is something you can do right now.

Extending the Content Pipeline Tool

One improvement you can make to the game is to extract the level text files from the solution, where LevelsLevelDatalevel1.txt is currently set as an embedded resource, and instead treat them as game assets. This allows level designers to contribute to the game without having to open the code and compile it whenever they make a change. They instead only need to open the text files in their favorite text editor, edit the levels, and then run the Content Pipeline Tool.

But because the content pipeline does not know how to turn a level text file into a Level object that can be used by the game, you need to teach it how to use the LevelReader to create an instance of a Level object.

To quickly recap how levels are loaded in the game, the LevelReader reads the text file, which is a grid of comma-separated numbers where a 0 indicates an empty tile and a 1 indicates the presence of a shooting turret. The last element at the end of every row represents an event. An underscore means nothing happens and a letter g followed by a number tells the game to generate that many numbers of choppers. It is very simplistic and will be improved upon in Chapter 4. For now, you will concentrate on making the level text files assets managed by the pipeline.

As you saw earlier, to create your own pipeline extension, you need to create a level importer, a level processor, a level writer, and a level reader. Once they are compiled into a library DLL, you tell the Content Pipeline Tool to reference that DLL and it will be able to scan it and offer you the new importer and processor.

Creating a Pipeline Extension

MonoGame has a template for creating pipeline extensions. In Visual Studio, create a new project and select the MonoGame Pipeline Extension template, as shown in Figure 2-5. Name your project PipelineExtensions.
../images/508651_1_En_2_Chapter/508651_1_En_2_Fig5_HTML.png
Figure 2-5

The MonoGame Pipeline Extension template

This will create a .Net 2.0 C# library project in your solution that contains two initial files: Importer1.cs and Processor1.cs. Before compiling the project, set it up in the solution to be compiled in Release mode, as seen in Figure 2-6. To configure solution properties, right-click the solution and click Properties.
../images/508651_1_En_2_Chapter/508651_1_En_2_Fig6_HTML.png
Figure 2-6

Setting your pipeline extension projects to be built in release mode

Compile the solution and then open the Content Pipeline Tool application. Click the top level Content item in the asset tree view to look at its properties. One of them, all the way at the bottom, is the list of references imported into the tool. It should look like Figure 2-7.
../images/508651_1_En_2_Chapter/508651_1_En_2_Fig7_HTML.png
Figure 2-7

The properties of the Content root item

Click the References property and a pop-up will open, as shown in Figure 2-8. Click the Add button and navigate to where your pipeline extension project’s DLL is being built. It should be in the PipelineExtensionsinRelease etstandard2.0 folder. Select the PipelineeExtensions.DLL file and click Okay to close the pop-up.
../images/508651_1_En_2_Chapter/508651_1_En_2_Fig8_HTML.png
Figure 2-8

Adding references to the Content Pipeline Tool

You should now see Importer1 and Processor1 in the drop-down options when selecting processors and importers for your assets. See Figure 2-9 for an example.
../images/508651_1_En_2_Chapter/508651_1_En_2_Fig9_HTML.png
Figure 2-9

Importer1 is now an option

Note that from this point on, you will need to close the Content Pipeline Tool application when you make changes to the PipelineExtensions project because it is using its DLL file and Visual Studio will not be able to overwrite it. Close it now since you are about to make changes.

Adding Logic to Your Extension

Let’s create your four pipeline building blocks. First, delete Importer1.cs and Processor1.cs. These two files will not be necessary anymore. Instead, create these four classes:
  • LevelImporter

  • LevelProcessor

  • LevelWriter

  • LevelReader

Your level importer will read the level text files and transform them into a single string. Then your level processor will take in the string and output a Level object that will be saved as an .xnb file by the level writer. Finally, the level reader will be in charge of reading in the .xnb level file and giving your game a Level object to work with.

This will change how the game code accesses and interacts with level assets. While we will not cover this part of the code in this chapter, please refer to the book’s source code for the full details of how the game code was modified to accommodate the new pipeline extensions. We will only go over the extension’s code in this chapter. It suffices to say that the responsibilities of reading in a level text file as a string and transforming it to a list of level events has been moved to the pipeline extension project in the Level.cs file.

Loading and Saving Assets
With that said, let’s take a look at your LevelImporter class :
[ContentImporter(".txt", DisplayName = "LevelImporter",
 DefaultProcessor = "LevelProcessor")]
public class LevelImporter : ContentImporter<string>
{
 public override string Import(string filename, ContentImporterContext context)
 {
 return File.ReadAllText(filename);
 }
}

The class inherits from ContentImporter<string>, meaning it will load the asset file and output a string. It is also marked with the ContentImporter attribute, which specifies that level asset files will be .txt files, that the Content Pipeline Tool application will use the name LevelImporter in the drop-down options and that the pipeline will pass the output string to the LevelProcessor class .

You then override the Import() function and use the basic IO.File.ReadAllText() function to read the entire .txt file, referenced by the filename parameter provided to you by the content pipeline when it calls your extension, as a string.

While you do not use the context parameter here, it can be useful for debugging purposes. Its properties can be used to inspect the intermediate and output directories of the current processor and the Logger property provides a logger that you can use to output messages while running the Content Pipeline Tool.

Next, your LevelProcessor will look like this:
[ContentProcessor(DisplayName = "LevelProcessor")]
public class LevelProcessor : ContentProcessor<string, Level>
{
 public override Level Process(string input, ContentProcessorContext context)
 {
 return new Level(input);
 }
}

The LevelProcessor inherits from ContentProcessor<string, Level>, which means that its overridden Process() function takes a string as input and will output a Level object. It is also marked by the ContentProcessor attribute with a DisplayName of LevelProcessor. Note that in the book’s source code, there is a Level class in the PipelineExtensions project and this is what the code is referring to here, not the Game’s Level class. The process of transforming a string into a Level is described in detail in the MonoGame Mastery book and its code has been migrated into the extension’s Level class.

Now let’s take a look at your LevelWriter so you can create the .xnb files, which will simply output the level as a string, the same string that was read from the original asset file. You could have been more precise and output the level grid’s dimensions, followed by each value of each row, but saving the string itself is more flexible at this point given that you will change the level assets in Chapter 4.
[ContentTypeWriter]
public class LevelWriter : ContentTypeWriter<Level>
{
 public override string GetRuntimeReader(TargetPlatform targetPlatform)
 {
 return "PipelineExtensions.LevelReader, PipelineExtensions";
 }
 protected override void Write(ContentWriter output, Level value)
 {
 output.Write(value.LevelStringEncoding);
 }
}

The LevelWriter is discovered by the content pipeline when the time comes to save the processed output into an .xnb file, because it is marked by the ContentTypeWriter attribute. A lot of work is done for you by the framework at this point, like creating the .xnb file and writing its headers. All you need to provide here is an overridden GetRuntimeReader() function that returns the full class name of the LevelReader class that will be used to read the .xnb file back, and an overridden Write() method that you can use to write the Level value to disk as a binary file.

Finally, here is your LevelReader class:
public class LevelReader : ContentTypeReader<Level>
{
 protected override Level Read(ContentReader input, Level existingInstance)
 {
 return new Level(input.ReadString());
 }
}

The LevelReader inherits from ContentTypeReader<Level>, which indicates that its Read() function will return an instance of the Level class. Read() takes two parameters: a ContentReader object that knows how to read different binary values and convert them into basic types, and an existingInstance of the object you are trying to create, which can be confusing. Before calling the Read() function , the pipeline will create a default instance of the desired object and pass it in as the existingInstance. Here, you are ignoring the parameter and creating a new instance of the level yourself.

Now that your pieces are in place, you can move your level files out of the code base and treat them just like any other asset. In the books’ source code, the evel1.txt file was moved to assetslevelslevel1.txt alongside the sounds, music, and texture assets. It was then added to the Content Pipeline Tool into the Levels folder, as shown in Figure 2-10.
../images/508651_1_En_2_Chapter/508651_1_En_2_Fig10_HTML.png
Figure 2-10

level1.txt is now an asset

Adding Animations to the Content Pipeline

Animations are another thing that can be moved out of the code and given to the Content Pipeline Tool to manage. The game already has animation sprite sheets managed as assets, but the details of the animation are in the code, which specifies each animation cell’s position, dimensions, and lifespan. In the original game, the PlayerSprite’s animations are defined in code as follows:
Animation _turnRightAnimation = new Animation(false);
_turnLeftAnimation.AddFrame(
 new Rectangle(348, 0, AnimationCellWidth, AnimationCellHeight),
 AnimationSpeed);
_turnLeftAnimation.AddFrame(
 new Rectangle(232, 0, AnimationCellWidth, AnimationCellHeight),
 AnimationSpeed);
_turnLeftAnimation.AddFrame(
 new Rectangle(116, 0, AnimationCellWidth, AnimationCellHeight),
 AnimationSpeed);
_turnLeftAnimation.AddFrame(
 new Rectangle(0, 0, AnimationCellWidth, AnimationCellHeight),
 AnimationSpeed);

Each animation only has four frames to define but specifying all of this in code for more complex animations with more frames will get cumbersome very quickly. Instead, you could move all that information into an XML file that lives as an asset right next to its associated sprite sheet asset. While you could store that information in a different format, like JSON or YAML, the Content Pipeline Tool comes with a built-in XML importer that you can use and thus avoid building another extension.

Before you build the XML asset, however, you define the data structure that will store the animation data in the code and then use it to create your XML file. This data structure must be referenced by the Content Pipeline Tool so it can access it. When it parses the XML file, it will instantiate it and populate its public properties automatically for you. This means the data structure must be in a library whose DLL is referenced by the Content pipeline Tool, similarly to what you did earlier.

You could add a new class to your PipelineExtensions project since it is already being referenced, but you would miss the opportunity to improve your game engine. Animations are already part of the engine and defining animation cell positions, dimensions, and lifespans should belong to the engine. So, the engine also needs a reference to your data structure. The only way to gracefully handle this scenario is to create a new MonoGame PipelineExtension project in your solution that will strictly be used by the engine and not your game. Go ahead and create the project as you did in the previous section and name it Engine2DPipelineExtensions. One day, when the engine is ready to be separated from the game into its own library, it will come with its own sets of pipeline extensions, which will include the ability to read animation data from an XML file.

In the new project, create a new class named AnimationData.cs :
public class AnimationData
{
 public int AnimationSpeed;
 public bool IsLooping;
 public List<AnimationFrameData> Frames;
}
public class AnimationFrameData
{
 public int X;
 public int Y;
 public int CellWidth;
 public int CellHeight;
}

The data structure above mimics what you see in the code. First, an animation is created with a flag that indicates if it is a looping animation or not. You also add the animation speed at the animation level instead of in the frames because animations tend to use the same speed for all their frames. Then you have a list of animation frame data that includes its position on the sprite sheet and its dimensions.

When the XML asset is read, it will create an AnimationData object that will include all the frames needed to perform the animation. But what will this XML look like and how do you create it? One quick trick you can use is to write code that creates an AnimationData object and saves it as XML, and then use the resulting XML file as your template to build the asset.

Creating the XML Template

Program.cs is the piece of code that starts your game, so let’s temporarily generate your XML template there. First, install the MonoGame.Framework.Content.Pipeline to the Game project. It will be used to serialize your AnimationData object into an XML file. Then, open Program.cs and add the following code at the beginning of the Main() method. Remember, this is all temporary. Once you have your XML template, you will undo all of this.
// using Microsoft.Xna.Framework.Content.Pipeline.Serialization.Intermediate;
// using System.Xml;
AnimationData data = new AnimationData();
AnimationFrameData frame1 = new AnimationFrameData();
AnimationFrameData frame2 = new AnimationFrameData();
frame1.X = 1;
frame1.Y = 1;
frame1.CellHeight = 1;
frame1.CellWidth = 1;
frame2.X = 2;
frame2.Y = 2;
frame2.CellHeight = 2;
frame2.CellWidth = 2;
data.AnimationSpeed = 1;
data.IsLooping = true;
data.Frames = new System.Collections.Generic.List<AnimationFrameData> {
 frame1, frame2 };
XmlWriterSettings settings = new XmlWriterSettings();
settings.Indent = true;
using (XmlWriter writer = XmlWriter.Create("animation.xml", settings))
{
 IntermediateSerializer.Serialize(writer, data, null);
}

This code starts with two comments that indicate which namespaces need to be imported into the code if you are not using Visual Studio to auto-discover them.

Here you start by creating a simple AnimationData object with dummy data. Then you use an XmlWriter and the content pipeline’s serializer to transform that data to an XML file. When you run the game, it will generate the template XML file for you before loading the game. All you need to do now is go fetch that XML file and delete all the code you just wrote. The file, located in GameinDebug et5.0 (or somewhere equivalent depending on which .Net version you are using), will look like this:
<?xml version="1.0" encoding="utf-8"?>
<XnaContent xmlns:PipelineExtensions="Engine2D.PipelineExtensions">
 <Asset Type="PipelineExtensions:AnimationData">
 <AnimationSpeed>1</AnimationSpeed>
 <IsLooping>true</IsLooping>
 <Frames>
 <Item>
 <X>1</X>
 <Y>1</Y>
 <CellWidth>1</CellWidth>
 <CellHeight>1</CellHeight>
 </Item>
 <Item>
 <X>2</X>
 <Y>2</Y>
 <CellWidth>2</CellWidth>
 <CellHeight>2</CellHeight>
 </Item>
 </Frames>
 </Asset>
</XnaContent>

The XML file starts with an XnaContent tag that indicates which assembly contains the class that you want to write the data to. It is followed by an Asset tag that is used to map the data below with a specific class that will receive it. From there, each tag represents a public variable on the class and lists, like the Frames list, contain a series of Item tags that are used to fill each frame in the list. With this information, you can move your PlayerSprite animation data into an asset.

Creating the TurnLeft Animation Asset

Create a new file named turn_left.xml in the assetsanimationsPlayer folder:
<?xml version="1.0" encoding="utf-8"?>
<XnaContent xmlns:PipelineExtensions="Engine2D.PipelineExtensions">
 <Asset Type="PipelineExtensions:AnimationData">
 <AnimationSpeed>3</AnimationSpeed>
 <IsLooping>false</IsLooping>
 <Frames>
 <Item>
 <X>348</X>
 <Y>0</Y>
 <CellWidth>116</CellWidth>
 <CellHeight>152</CellHeight>
 </Item>
 <Item>
 <X>232</X>
 <Y>0</Y>
 <CellWidth>116</CellWidth>
 <CellHeight>152</CellHeight>
 </Item>
 <Item>
 <X>116</X>
 <Y>0</Y>
 <CellWidth>116</CellWidth>
 <CellHeight>152</CellHeight>
 </Item>
 <Item>
 <X>0</X>
 <Y>0</Y>
 <CellWidth>116</CellWidth>
 <CellHeight>152</CellHeight>
 </Item>
 </Frames>
 </Asset>
</XnaContent>

This file follows the same format as your template, but is filled with the data used in original game code used to create the _turnLeftAnimation animation. Now add this file to the content pipeline tool, set the importer to the “Xml Importer – MonoGame,” the processor to “No Processing Required,” and build the assets, which should build the .xnb file for the animation. All that you need to do now is load it as game content and build the animation from the AnimationData object.

Add a new function to load your AnimationData to the BaseGameState class :
protected AnimationData LoadAnimation(string animationName)
{
 return _contentManager.Load<AnimationData>(animationName);
}
Add the following private constant to the GameplayState class :
private const string PlayerAnimationTurnLeft =
 "Sprites/Animations/FighterSpriteTurnLeft";
Then load the animation by calling and pass it to the PlayerSprite object via a new constructor parameter:
var turnLeftAnimation = _contentManager.Load<AnimationData>(animationName);
public PlayerSprite(Texture2D texture, AnimationData turnLeftAnimation)
 : base(texture)
{
 // ... code edited out
 _turnLeftAnimation = new Animation(turnLeftAnimation);
}
Then you need to create a new Animation constructor that can be called with an AnimationData object:
public Animation(AnimationData data)
{
 _isLoop = data.IsLooping;
 foreach( var frame in data.Frames )
 {
 AddFrame(
 new Rectangle(frame.X, frame.Y, frame.CellWidth, frame.CellHeight),
 data.AnimationSpeed);
 }
}

Finally, after doing the same thing for the turnRightAnimation, running the game should give you the exact same behavior as the original game. While you have not improved the game itself, you have added flexibility into how you build your game assets and decoupled animation details from the code.

Internationalizing Game Text

The last thing you will tackle in this chapter is how to use the Content Pipeline Tool to automatically create SpriteFonts that can be used to draw text in any language. The game you are improving did not have any internationalization in place and the text displayed on the screen is only in English. You will improve on this in two steps:
  • Use string resources for each language the game will support and let the .Net Framework localize your game appropriately.

  • Use the Localized Font Processor to generate Unicode SpriteFonts.

The original game’s Content Pipeline Tool only generates SpriteFonts for all the English characters when you build your assets. For each character of the English language, the content pipeline generates a texture that will be used to draw that character on the screen. But why not create textures for all existing Unicode characters? Because this would take a very long time. So, you need a different strategy that will build Unicode textures for only the characters that are used in the game. This is done by using the Localized Font Processor for your fonts, as seen in Figure 2-11. What this processor does is look at the SpriteFont XML file for the ResourceFiles tag, which lists all the .Net resource files you have in your project.
../images/508651_1_En_2_Chapter/508651_1_En_2_Fig11_HTML.png
Figure 2-11

Selecting the Localized Font Processor

For each resource file, it looks at all the strings in the file and only creates textures for the characters in the string.

So, let’s start by creating your resource files and getting the .Net localization system involved. As shown in Figure 2-12, right-click the Content folder in the Game project and add a new Resource item called Strings.resx1.
../images/508651_1_En_2_Chapter/508651_1_En_2_Fig12_HTML.png
Figure 2-12

Adding your first resource

Once the resource has been added, double-click it and edit the resource file so that it has everything, as shown in Figure 2-13.
../images/508651_1_En_2_Chapter/508651_1_En_2_Fig13_HTML.png
Figure 2-13

English text

You can see that this has all the text needed by the game so far. Now let’s change the code to use this resource file instead of the hardcoded strings in the code. Replace any instance of the text in the resource file with the corresponding resource. For example, this line in GameplayState.cs changes from
_levelStartEndText.Text = "Good Luck, Player 1!";
to
_levelStartEndText.Text = Strings.GoodLuckPlayer1;
Then, edit Program.cs to add this to the start of the Main() method:
Strings.Culture = CultureInfo.CurrentCulture;

This sets the culture of the app to the current culture of the machine that is running the game.

You now need to add some text translations. Create two new resource files in the Content folder called
  • Strings.fr.resx

  • Strings.ja.resx

By using this format, the .Net Framework will automatically choose the right Strings resource file based on which culture is set for the game. The French and Japanese cultures are codified by the “fr” and “ja” codes, so if you set the game to those cultures, .Net will use the codes to find the right resource file. If a resource file is not found for a certain code, then .Net will default to the Strings.resx file , which uses English text.

See Figures 2-14 and 2-15 for the content of the resource files in French and Japanese. Please note that I do not speak Japanese, so I used Google Translate to get the Japanese strings.
../images/508651_1_En_2_Chapter/508651_1_En_2_Fig14_HTML.png
Figure 2-14

French text

../images/508651_1_En_2_Chapter/508651_1_En_2_Fig15_HTML.png
Figure 2-15

Japanese text

Now back in Program.cs, you can force the game to use the French or Japanese language by forcing the game culture to those languages, for example, by replacing the line of code that sets the current culture with this line:
Strings.Culture = CultureInfo.GetCultureInfo(JAPANESE);
Build and run the game and you will see that you have one more problem, however. Instead of Japanese characters, you have blank boxes, as seen in Figure 2-16.
../images/508651_1_En_2_Chapter/508651_1_En_2_Fig16_HTML.png
Figure 2-16

Blank text fonts

This is because you are using the Arial font, which, as installed on my Windows machine, does not have Japanese characters. When the Content Pipeline Tool builds the font textures, it rasterizes the font found on the builder’s machine into a texture. But if the font does not have the desired characters, then a blank box is instead rendered into the texture.

The easiest solution is to change the font from Arial to MS Gothic in the SpriteFont files, which does include Japanese characters. Unfortunately, however, its English characters are not very pretty. So, a better solution is to download a better font from the Internet. The Noto Serif JP font is freely available here: https://fonts.google.com/specimen/Noto+Serif+JP. Download and install it, and then change the font from Arial in all the SpriteFonts in GameContentFonts to this new font:
<FontName>Noto Serif JP</FontName>
Rebuild the project and you should see Japanese text, like in Figure 2-17.
../images/508651_1_En_2_Chapter/508651_1_En_2_Fig17_HTML.png
Figure 2-17

Japanese text correctly rendered on the screen

Conclusion

In this chapter, you took a deep dive into the Content Pipeline Tool and how its four building blocks (the content importer, the content processor, the content writer, and the content reader) all work together to take an asset file and produce a reusable game asset. You then extended the Content Pipeline Tool so it can process level files and animation details as proper game assets. Finally, you set up your game to be translated into many different languages using the Localized Font Processor and resource strings.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.139.81.58