Tuesday, November 24, 2009

Non-Trivial Custom ContentProcessor - Part 2

If you're reading this for the first time you might want to check out the Introduction and Part 1 first.

The next step taken by the XNA Content Pipeline is to process the data produced by the content importer. This is done by the aptly named ContentProcessor classes. These classes are responsible for pretty much anything that happens between the content being loaded (via ContentImporter or ContentReader which I'll discuss an a later article) and the content being used.

ContentProcessors are used to massage data into a format usable by an XNA project and/or to load additional content. For example a model processor will load textures and shaders that the model is rendered with. So processors usually run when importing content into a game project or when loading imported content as part of game execution.

As I mentioned in Part 1, my custom content importer simply converted the raw XML input into a nice tree that can be worked with by the processor (using the .NET System.Xml family of classes.) Now this XML has to be turned into something meaningful to the game. This obviously leaves a lot of work to the content processor.

As I mentioned before this is the structure of the XML:
  • Group (document element)
  • One or more entity elements. Each element defines how it's related to the others. In-game this is used to build an n-tree of entity objects with one entity as the root.
  • One or more property elements for every entity. The meaning/use of these properties is dependent on the type of entity.

What we'll do in the processor is iterate through all "entity" elements in the document, read in the data we care about, and add each to a content data class called EntityGroupTypeContent. The code to do this is relatively straigtforward and uninteresting. I'm sure it can be improved but at the time I wrote it my main intention was for it to work.

outputGroup = new EntityGroupTypeContent();
XmlElement top = input.DocumentElement;
foreach (XmlElement entity in top.GetElementsByTagName("entity"))
{
// Read in entity attributes (convert from string as needed)
string name = entity.GetAttribute("name");
string parentName = entity.GetAttribute("parent");
string entityType = entity.GetAttribute("type");
string renderObjectName = entity.GetAttribute("render-object");
string[] rawPosition = entity.GetAttribute("position").Split(',');
Vector3 position = new Vector3(Convert.ToSingle(rawPosition[0]),
Convert.ToSingle(rawPosition[1]),
Convert.ToSingle(rawPosition[2]));

string[] rawOrientation = entity.GetAttribute("orientation").Split(',');
Quaternion orientation = new Quaternion(Convert.ToSingle(rawOrientation[0]),
Convert.ToSingle(rawOrientation[1]),
Convert.ToSingle(rawOrientation[2]),
Convert.ToSingle(rawOrientation[3]));

// Read entity properties
Dictionary properties = new Dictionary();
foreach (XmlElement property in entity.GetElementsByTagName("property"))
{
string propertyName = property.GetAttribute("name");
EntityGroupPropertyContent propertyData = new EntityGroupPropertyContent(property);
properties.Add(propertyName, propertyData);
}

// Add the entity
outputGroup.AddChildEntity(name, parentName, entityType, renderObjectName, position, orientation, properties);
}

Notice the lack of exception handlers on Convert.ToSingle. If something goes wrong here the exception percolates up to the content builder. When this happens the exception is recorded as a build error in Visual Studio. Also notice the EntityGroupPropertyContent instantiation. It's pretty important but talking about it brings up a lot of discussion irrelevant to this entry. I'll talk more about EntityGroupPropertyContent in Parts 3 and 4.

Next we have to validate the data that was loaded. The game expects the following conditions to be true:

  • The group has a root entity. Its parent entity is undefined
  • There is only one root entity.
  • Entities within the group are only related to eachother, not other groups
  • Entity properties are valid (notice valid here isn't defined yet, stay tuned :)

Since we didn't verify these conditions in the importer they have to be checked now. This way any errors in the content will be caught by the compiler and show up as build errors. Trying to catch these errors later would result in them crashing the game.

I'll get into validation with Part 3.

Sunday, November 15, 2009

BOOM!

Nobody can resist explosions.

Non-Trivial Custom ContentProcessor - Part 1

This is part one on my series on writing a non-trivial custom content processor for XNA Game Studio 3.1. If you want you can read the introduction of this series for useful background information on why I went about writing a content processor.

Besides the tutorial and samples I mentioned in the introduction MSDN also has a very nice tutorial on how to create a complete content processor.

A complete content pipeline extension project consists of the following parts:
  • A content importer
  • A content processor(s)
  • A content data class(es)
  • A content writer
  • A content reader

Each piece is a distinct step in the process of importing and loading content in your project.

The content importer is a very simple object. All it has to do is open the basic content (image, model, game data etc.) and parse it enough that it's usable by the content processor. Since I originally chose to use XML my importer only needs to open the file as an XML document. If you're using XML you might want to test the document against a DTD to save in sanity checking code in the processor.

Here's the importer:

using System;
using System.Xml;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content.Pipeline;
using Microsoft.Xna.Framework.Content.Pipeline.Graphics;

// Entity group type specs/annotations are in XML format
using TImport = System.Xml.XmlDocument;

namespace EntityGroupProcessor
{
///
/// Basic entity group type importer. Entity group types are always in XML. All this does
/// is load the file.
///

[ContentImporter(".ent", DisplayName = "Entity Group Importer", DefaultProcessor = "EntityGroupTypeProcessor")]
public class EntityGroupTypeImporter : ContentImporter
{
public override TImport Import(string filename, ContentImporterContext context)
{
XmlDocument document = new XmlDocument();
document.Load(filename);

return document;
}
}
}

Notice the line
[ContentImporter(".ent", DisplayName = "Entity Group Importer", DefaultProcessor = "EntityGroupTypeProcessor")]
The first part ".ent" specifies the extension of the file that this importer processes. This allows Visual Studio to automatically use this importer when content with the extension of ".ent" is added to the project. The second part specifies the name of the importer as it appears in Visual Studio. The third, and most important part, names the class used to process the output (in this case an XmlDocument instance) in the next step.

The bulk of the code, at least in my case, ends up in the content processor and the content data classes. Stay tuned for Part 2 where I get into the basics of the ContentProcessor and content data classes.

Neat effect

I've been trolling around for information on how I could make a better looking beam weapon. One idea I had was to render them as volumes instead of flat lines. In my research I stumbled across Graphics Runner who has some neat tutorials posted on rendering volumetric effects on his blog. Check 'em out.

I'm likely to not use 3D textures like in these blogs so that I can save on resources. Instead I hope to derive the thickness of a particular section of a beam from the alpha value of that section of beam.

Stay tuned for followups, and Part 1 of my custom content processor series.

Saturday, November 14, 2009

Non-Trivial Custom ContentProcessor - Intro

The network component is finally done for this sprint. Yay! And I have a project that compiles for the X-Box 360. Yayy!!

This past week has been interesting so far since I found that my initial solution for batching objects for drawing was impractical when it came to the critical stage of actually doing the batching. So I had to tear it out and start over. The new solution is quite clean and has an added advantage of reducing the amount of data stored for render objects. Only one set of render data is stored no matter how many objects that use it exist. But this change started a cascade of extra changes which has led me to begin developing a custom content processor for XNA.

I'm working on generalizing the code needed to spawn in-game objects. My original method of loading in-game object data was to load a model and create one in-game object for each mesh that makes the model. The properties of the in-game objects were encoded in the names of the meshes. Initially this worked surprisingly well. The biggest advantage is that I didn't have to spend time on tool development and could focus on getting the game working.

There are, however, four problems.
  • No two objects defined in a model can have the same properties (due to mesh names having to be unique)
  • There's no easy way of creating a deep tree of attached objects (e.g. a capital ship with turrets)
  • Using names severely limits the number of properties that can be defines
  • Not all objects need to be models (e.g. beams and bullets)

My solution is to split off loading of graphics and defining in-game objects. Graphics are loaded and tossed into a pool for later use. Each graphical item has a unique name which can be used to reference it. Once the graphics are loaded the game then has to load information on how to create in-game objects using the graphics.

This is where the ContentProcessor comes in. My current plan is to create a custom XML document which defines a tree of in-game objects and their properties. Each file lists a set of in-game objects to be created. Each object is a single block in the XML file defining the following.

  • What appearance the object has (references a loaded bit of graphics)
  • What the object's type is
  • What the object's reference name is
  • What the object's parent's reference name is
  • A set of properties specific to the object's type

In order to keep the code for the game clean this XML data is to be loaded using a custom ContentProcessor. There are at least some resources out there on the internet including this (tutorial 21, PDF Alert!) and this which provide some nice sample code. I like the first since it distills the process down to its simplest form. Useful for learning, but it doesn't cover all the nuances I need for what I'm doing.

Stay tuned as I post more details on the content processor.

Monday, November 9, 2009

What to do... what to do... what not to do

Earlier in this blog I mentioned that Space Combat Sim is being developed using at least the trappings of the Scrum methodology. I say the trappings because certain aspects of the method don't apply because A, I don't have a team to hold meetings with and B, the Scrum methodology seems to assume a 40 hour week of five 8 hour days. This is great for regular software! I love having 8 hour days. They certainly beat 10 and 12 hour days (grumble grumble.)

But I digress... one part of Scrum that we do use is the product backlog. It's essentially a glorified to-do list. A huge set of items to be created for the project with estimates for how long they'll take. Work from the list is batched together in units called sprints. The expectation that over the length of the sprint all of the selected tasks are to be done. In the real world I believe sprints are one or two weeks long, in school they're about a month. Anyway, if the to-do list for the sprint isn't empty when the time is up then there's a problem. Fortunately, due to the way Scrum works this problem is noticed as the sprint ends which is usually well before the project is to be delivered. This is the good news.

The bad news is that, in my case, I have around 40 hours of work left to accomplish in one week. We're expected to put in about 12 hours a week individually. As I hinted before, that's a problem. At this point in the project I can defer some of the work to later sprints if need be. But first, prioritization.

The critical items (not started):

  • X-Box 360 support
  • Beam weapon rendering
  • Beam weapon test
  • Capital ship gun turrets (guns which can independently shoot targets)
  • Targeting cursor/crosshairs

The incomplete items:
  • The targeting computer
  • Weapon status display is working but ugly
  • Smoke/flames/tracers (barely started)
  • Sorting/grouping of graphics by how they're to be drawn
  • The network code (still untested)
So now that I have what I want to do I need to decide what I need to do. My primary goal for this phase is to get the fundamentals of multiplayer working and to get the game on the X-Box. I also want there to be enough supporting code done that I can get shooting and killing other players working in the next sprint.

This leaves:
  • The network code
  • X-Box 360 support

This cuts the amount of work left from ~40 hours to 10.5 hours or so. Much better.

Sunday, November 8, 2009

I can haz multiplayer test???

Over the last week I've been working on getting the network code implemented for Space Combat Sim. This is one of the most critical parts for the game since without it there's no way for the game to support multiplayer over Live.

This has turned out to be painful in one place which I didn't expect, the game's rules system. When I first created the game I've been working with a basic startup sequence which creates a bunch of fighters and assigns a local controller (i.e. your X-Box 360 gamepad) to one of them leaving the rest uncontrolled. I originally wanted to create a fairly simple stub and hook joining players to other objects arbitrarily. This didn't work.

The problem is that when a player joins they initially know nothing of the game's state. This includes which fighter they're supposed to control. If I just assigned one arbitrarily on both sides the results would be inconsistent. Each player would think that the other is attached to a different object. Worse, two players could be trying to control the same object. This meant I had to implement code which runs on the game's host only to assign a player to a specific in-game object. In addition it had to support the player being killed and assigned a new object of (possibly) a different type than before.

What occurs now is this:
  1. The host starts up a game session and starts playing.
  2. Some other player joins the game session
  3. The host notices the joining player and asks the rules system that a new player is in the game. This is where team balancing etc. would occur.
  4. In the mean time the joining player is watching the game without participating.
  5. The host notices that there's a player without a ship (i.e. the joining player.) The host finds an appropriate place to create the player and adds them to the game. In addition the host lets all players know that one of the players has just been assigned a new ship. This is where re-spawn rules would go.
  6. The joining player notices that they've been assigned a ship. Since it doesn't exist on their machine yet they create it and attach their local controller to it. The player is now in the game.
  7. If there were other players they'd also notice a player has been assigned a ship. Since it also doesn't exist for them they'd create it as well.

I've omitted some housekeeping but this is, at least for now, how players joining and being assigned ships is handled. What's left is testing. Since I only have one PC on hand I'll need to do that at school.

Monday, November 2, 2009

Oink

Normally in these entries I drone on about some random technical crap that is not terribly interesting but last week I didn't actually do any technical crap... so I don't have anything to talk about.

Instead, last week, I was down with a nice little case of the swine flu. Not fun... but not the worst disease I've had by any strech however. The only thing that really sucked was the night I didn't get to sleep (and fitfully at that) until about 2:00AM because of an irritating, persistant cough. That and the fact I had not one, but two exams right in the middle of the week I was sick. Oh yeah, that fever was fun too...

To anyone reading... try to not get the swine flu, it's not the worst flu, but it still sucks.

I'll be posting about my adventures in getting communication between players working later this week.