Unfortunately, his mistake was that he fell into the trap of .NET's XmlSerializer. It is an easy trap to fall into because it is so simple and convenient to use. Besides, the XNA Custom Content Pipeline is not advertised well enough and is actually quite obnoxious to use correctly (and the documentation is rather dull). Luckily for all of my two readers, I have read that documentation already!
In today's example, I'm going to be operating on the wonderful game of Wumpus World. I'm going to be making visualization and editor for the game, the reader can tie in input controls or whatever to make it a full game.
Setting Up the Project
First things first, we need to set up the project. Obviously, you'll want to start off with your XNA Game - mine is called "Wumpus." To make a custom pipeline extension, right-click your solution, perform the clicks to add a new project, and in the "XNA Game Studio $version" section, find "Content Pipeline Extension Library." The convention here is to append ".Pipeline" to the namespace of your project, so mine is named "Wumpus.Pipeline." There is automatically a ContentProcessor which is so creatively titled ContentProcessor1. I'm not sure who at Microsoft made the decision to put these useless default files in every project, but you can just delete it.
Anyway, to actually use the things in your pipeline extension, your content project in the XNA game needs to have a reference to the pipeline project. To do this, right click the content project, click "Add Reference," then go to the "Projects" tab of the dialog that comes up and select "Wumpus.Pipeline" (or whatever you named your pipeline project).
So now your content project can build with the pipeline extensions, but how do we build the objects in the extension library? You'll most likely want to use the objects you've already created in your library, so try to add a reference to your main game through the same method. I say "try to add" because this will not work. It will tell you that you have a circular dependency. Why? Right now, there are three projects in your solution:
- Wumpus -- This implicitly references Wumpus (Content).
- Wumpus (Content) -- This references Wumpus.Pipeline.
- Wumpus.Pipeline -- This cannot reference Wumpus, because that would make a loop.
The solution is to create another project containing the core components of your game and reference that from both the game engine and the pipeline extension. This avoids any possibility of circular references. So add a new Windows Game Library to your project. The convention is to append ".Core" to the name of the library. As you probably know, Visual Studio will assume that is the namespace you would like to use for all classes in the project. However, you won't be putting classes in the Wumpus.Core namespace, so you can change that behavior by opening the properties window for the core library and changing the "Default Namespace" to Wumpus (or whatever you want).
So here is the solution structure as it stands:
- Wumpus.Core -- This has it's own content project, but you don't need to put anything in it.
- Wumpus -- References Wumpus (Content) and Wumpus.Core.
- Wumpus (Content) -- References Wumpus.Pipeline.
- Wumpus.Pipeline -- References Wumpus.Core.
Originally I had mentioned an editor which uses the in-game engine and all the objects, so let's do that. Add another project to your solution, but this one is going to be a WPF Application (you could also do a Windows Forms application, but WPF makes GUI creation fun and easy). I named mine "Wumpus.Editor." Add a reference to Wumpus.Core (since you're going to be using objects from it) and Wumpus (since we want to use the engine components from that).
As clearly indicated from my wonderful drawing, Wumpus.Core is the center of the attention.
Code Architecture
In the interests of brevity, I'm going to leave out the parts of the code that are boring. They're all part of the download package at the bottom of this post. Basically, there are two classes: TileQuality, which is an enum with qualities such as breezy, gold, stinky and wumpus with bitwise flags and WumpusWorld, which contains a grid of TileQualitys. They both reside in Wumpus.Core, since they are needed for all phases of design
Being able to compose a full application by integrating many small pieces and making them communicate is wonderful. However, it requires strict adherence to the one object serves one purpose philosophy of object-oriented programming. Unfortunately, the Microsoft.Xna.Framework.Game class is often a blatant violation of this philosophy, as it encourages putting all the drawing, user interaction, asset management and everything else into this one place. Knowing this, it is important to put different parts of the application in different places. For this demo, the only part that will be on its own will be the graphics engine.
So, create a new class in Wumpus (because that is where engine things go) called GraphicsEngine (creative, I know). In a real project, you might want to make an interface like IEngineModule or something so that you can easily manage all the engine parts, but you can just implement IDisposable. Since we're abstracting things, we question: What does a graphics engine need? I'm thinking it needs a reference to the GraphicsDevice so that it can do its job and a ContentManager so that it can locate and load graphics resources. This is my constructor signature:
public GraphicsEngine(GraphicsDevice device, ContentManager manager);
This gives us the freedom to use the graphics engine any time we have those two items, instead of relying on the engine to go out and discover those things itself (which is a very easy way to accidentally create class coupling). This is the basis of dependency injection. Of course, if we wanted to be real legit about this, we could plug in a real dependency injection framework and work out what needs what from some configuration, but that's overkill here.
The sole job of the graphics engine is, given a WumpusWorld, draw it to the display device:
public void Draw(WumpusWorld world);
We are going to call this from our game, so we will need to have an instance of GraphicsEngine in our WumpusGame class. Following the XNA patterns, Initialize is probably the best place to create the graphics engine, since we should have a reference to a valid device (assuming you've made a GraphicsDeviceManager and initialized it, as per the default):
m_graphics = new GraphicsEngine(GraphicsDevice, Content);
So instead of performing the drawing ourselves in WumpusGame.Draw, we delegate this task to the GraphicsEngine and trust it to do everything right:
protected override void Draw(GameTime gameTime)
{
m_graphics.Draw(m_world);
}
Pretty easy, huh? Of course, m_world is null right now, so you will want to instantiate it somewhere (doing it in LoadContent makes the most semantic sense). If you have the free time, you can edit the world by hand and play the game:
I know, the artwork is amazing.
Saving and Loading
As much fun as it is to create these worlds in code, it would be nice if we could persist these things outside of code. For many things, .NET's automatic XML serialization is a wonderful thing and can be used. However, my WumpusWorld class has a two-dimensional indexed property (public TileQuality this[int row, int col] { get; set; }), which cannot be serialized, so I wrote my own methods for saving and loading:
public void Save(Stream stream);
public static WumpusWorld Load(Stream stream);These methods will be called by anything that wants to save or load for the simple text format (hereby referred to as .wump). Here is some example output:
4,4
None
Breeze
Breeze
Pit
Breeze
Pit
Breeze, Stench
Breeze, Gold
None
Breeze, Stench
Breeze, Wumpus
Stench
Dude
Breeze
Pit
Breeze
Originally, though, I had talked about extending XNA's content pipeline, so we're going to make a reader and writer to convert to and read from the XNB binary format. This lets you do fun things like verify every single file at build time, take advantage of automatic compression and deployment and a slew of other features gained by drinking the XNA Kool-Aid. OOOH YEAH!
- ContentImporter: Reads an object from disk into memory. Input is a file name and the output is some .NET object. In our case, we're reading a .wump text file and outputting a WumpusWorld.
- ContentProcessor: Takes a .NET object, runs some operations on it and outputs some other .NET object. You can perform whatever arbitrary modification to the object you need to. We are not using it here, but I'm mentioning it because these can be very helpful (Shawn Hargreave's Pre-Multiplied Alpha Processor is a good example).
- ContentTypeWriter: Takes the .NET object that has been imported and optionally processed and writes it to disk with a binary serializer. The input is the processed .NET object and the output is a binary serialized file. This is the last step that is taken by the pipeline project.
- ContentTypeReader: Reads from the binary serialized stream and outputs the .NET in-game object. This is in your actual game engine code.
[ContentImporter(".wump", DisplayName = "Wumpus World Importer")]
public class WumpusWorldImporter : ContentImporter<WumpusWorld>
{
public override WumpusWorld Import(string filename, ContentImporterContext context)
{
// use the built-in load function
return WumpusWorld.Load(File.OpenRead(filename));
}
}
Simple, eh? The ContentImporter attribute lets Visual Studio know that when we add a .wump file to the content build project, we would like to automatically use this content importer and the text to display the name of the importer with. Look in the code for all the other class, but there is not much that needs explanation. Most of the ugly stuff is out of the way so that you are presented with this nice, clean interface.
Create a .wump file (might I suggest using the "example output" from a few paragraphs ago?) and adding it to the Wumpus's content build project. If everything worked properly, Visual Studio will associate the importer, not attach a processor, locate the writer at build time and associate the correct reader when the ContentManager needs it. In short, you will be able to load a WumpusWorld with a simple call like this:
m_world = Content.Load
Pretty slick stuff.
The Editor
So, we would really like people to be able to create new worlds not by editing some text file, but by using a GUI tool. As great as the NeoForce Controls are, they just do not have the sheer amount of things in the .NET forms nor are they as easy to design with (unless someone has made a WYSIWYG editor). Wouldn't it be great to somehow embed the GraphicsEngine that we have already made into a heavyweight GUI framework? In this demo, I'll be using WPF, because it is shiny, new and fun to work with.
The classic way to work with WinForms is to use Microsoft's WinForms Graphics Device, which is quite helpful. This allows you to make an XNA GraphicsDevice render to the surface of a regular control. However, this relies on the control having a handle, which is not present WPF (at least in a way that you can see). This is easily solved through use of WindowsFormsHost, which lets you host a WinForms control in a WPF environment.
On initialization, attach our world control as the child element of the host:
ctl_worldView = new WumpusWorldControl();
m_graphicsService = GraphicsDeviceService.AddRef(ctl_worldView.Handle,
ctl_worldView.Width,
ctl_worldView.Height);
ctl_formsHost.Child = ctl_worldView;
The final output of my editor looks like this:
Stunning beauty.
Code Files
The code here is not at all a "finished product." It would be pretty easy to extend the editor by doing things like adding smell and breeze automatically when a Wumpus or pit is added or making a world larger than 4x4.
It is released under the Apache License, Version 2.0 and should be used for good, not evil.
Wumpus Source Code
hi. Wumpus Source Code link is not workin can you give me its source code. i need some help fron it.
ReplyDeleteAfter you finally made it to the counter don't be surprised to find out that the lady behind the counter cannot understand anything of what you are saying, so you have to wait for someone speaking a foreign language (in the mean time the "néni" with the supermarket shopping bag waiting for her turn after you is mumbling something in Hungarian... not nice I guess). Apartment in Budapest for rent
ReplyDelete