Saturday, January 16, 2010

Non-Trivial Custom ContentProcessor - Part 3

So today I'm writing another exciting edition on how I built my custom ContentProcessor for loading game objects. If you've just started reading this series you might want to check out the Introduction, Part 1 and Part 2 first.

When I left off in Part 2 I provided a snippet of code to convert an XML document tree into a set of custom class instances which the game can work with easily. These classes describe how to create a group of in-game entities that make up a starship or other object. However the game makes some assumptions about the data these classes contain which can (and must) be verified by the ContentProcessor.

  • 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 :)

If we don't ensure these assumptions are valid then Space Combat Sim will crash when using the processed content. So, the next step that happens is validation and occurs immidiately after all the repeated process shown in Part 2 is finished.

bool foundParentEntity, foundTopEntity = false;
foreach (Entity entity in Entities)
{
if (entity.ParentName == "")
{
foundTopEntity = true;
continue;
}

foundParentEntity = false;
foreach (Entity parent in Entities)
{
if (parent.Name == entity.ParentName)
{
foundParentEntity = true;
break;
}
}

if (!foundParentEntity)
{
throw new ContentLoadException("Entity " + entity.Name + " references non-existant parent " + entity.ParentName);
}
}

if (!foundTopEntity)
{
throw new ContentLoadException("Group has no top-level (blank parent) entity!");
}

So, what does this mess do? It verifies that two of the four conditions are true. The outer loop's main purpose is to locate and flag that there is one root entity in the group. The inner loop ensures that every entity (besides the root) has a parent. After the loops is a check to verify that the root was indeed found.

Astute readers might notice a bug in the code above. Circular relationships, e.g. entity A is a child of entity B which is a child of entity A, are not prevented by this procedure. There are a couple ways of dealing with this. The simplest, and most evil, would be to recusively search for the root entity from a given entity. This causes a circular relationship to blow the stack and throw an exception. This can be prevented by making the recursive algorithm build a list of each entity visited as we go up toward the root. If an entity appears in the list more than once then a circular relationship exists. There are also non-recursive ways of doing the same thing which I'll leave as an exercize for the reader.

I decided to omit this step from the game since such errors are rare and are caught in the entity creation tool which you can see in my article on collision detection. Currently by crashing horribly, I'll fix that in the future.

Another apparent bug is the lack of testing for non-unique names and for multiple roots. These are both handled earlier. If you remember Part 2 the last part of loading an individual entity block from XML was a call to AddChildEntity.

foreach (Entity entity in Entities)
{
if (entity.ParentName.Length == 0 && parentName.Length == 0)
{
throw new ContentLoadException("Multiple top level (blank parent) entities found!");
}
if (entity.Name == name)
{
throw new ContentLoadException("Multiple instances of " + name + " found!");
}
if (entity.Name.Trim().Length == 0)
{
throw new ContentLoadException("Found nameless/whitespace named entity!");
}
}

The actual adding part has been omitted since it isn't that interesting.

With all of this taken care of we still have one item left to validate. The entity's creation parameters. In fact, I have yet to show you how they're loaded. In fact, this is because the main tradeoff is made for thoroughness of validation versus speed and flexibility of coding is made here.

Entities represent a wide range of object types in the game with wildly different rules. Simple objects include stuff such as hull sections which can do little besides be blown up. Others, such as weapons, are much more complex. Parameters are needed to define stuff like timing, positions, orientations and references to other game data. What's worse is that, at the time I was coding this loader, many properties had yet to be determined, stuff like sound effects and so on.

With all of that in mind I decided to err on the side of flexibility. The loader will accept all parameters and ensure that the XML data is valid for the type that is being used to fill the parameter. It will not check if the parameter is used by the game and also won't check if the parameter's type is what is needed by the game. I'll show you how I implemented this validation without adding an excess of validation code to the game itself in Part 4.

No comments:

Post a Comment