Friday, August 31, 2012

XNA and Level Validation

In the previous post, we discussed the topic of XNA and System Testing.
Now, let's build on this information to discuss Level Validation with XNA.

Note: this post includes complete code sample on CodePlex.
Download code sample here.

Level Validation
In game development, level data is most often stored in text files. For example, the Platformer starter kit contains 3x levels, although an unlimited number of levels could be added using data driven design.

Level data can then be validated through system tests to ensure all data is true and correct, and that all rules are observed before the level is actually loaded into the game.

Example
As an example, let’s revise the Platformer starter kit to demonstrate XNA and Level Validation.
First, create a system that can parse text files on demand; use the interface from previous post:
public interface IFileManager
{
 IList<String> LoadTxt(String file);
 T LoadXml<T>(String file);
 XElement LoadXElement(String file);
}
Next, define all the rules for each Level in the game; for example, each Level must have:
  • Valid characters only for player, enemy, tiles + gems
  • Same number of tiles width for each row in level
  • Exactly one entry for player - no more, no less
  • Exactly one entry for the exit - no more, no less
  • A passable tile immediately above the exit
Sample
The following code sample refactors the starter kit to integrate an unlimited number of levels using data driven design and validates each level through system tests.
First, define a simple text file that will store all levels to load in the game and in correct order.
Next, add all the corresponding level data for all levels in the game; one level per text file:

E.g. Level 10
...........G.G.G.G..
...1............A...
..######..########..
.....G.G.....G.....G
......B.............
G...-----...--.G..--
....................
--.G.G........--...G
....C........G......
..----.G.G..--....--
........D...........
.......----...--....
.G.G.........G.G.G..
..X.................
######......########
Add a method to the Level class to validate all level data according to the rules as defined above:
public class Level : IDisposable
{
 public void Validate()
 {
  if (null == levelData)
  {
   throw new Exception("No level data loaded.");
  }

  string validCharacters = ".XG-ABCD~:1#";

  // RULES:
  // Ensure only valid characters parsed.
  // Ensure same tile width for each row.
  // Exactly one entry for player!
  // Exactly one entry for exit!
  // Ensure exit point passable!
 }
}
Finally, write system tests to validate all data before fully integrating into the game.
Note: an IoC Container will be used to construct all components used throughout.

FILE MANAGER TESTS
[TestFixture]
public class FileManagerTests
{
 // System under test.
 private IFileManager fileManager;
 
 [SetUp]
 public void SetUp()
 {
  fileManager = IoCContainer.Resolve<IFileManager>();
 }

 [Test]
 public void LevelDataValidateTest()
 {
  String levelsFile = GetPath("LevelData.txt");
  levelIndexes = fileManager.LoadTxt(levelsFile);
  numberOfLevels = levelIndexes.Count;

  // There must be at least one level!
  Assert.IsTrue(numberOfLevels > 0);

  for (Int32 levelIndex = 0; levelIndex < numberOfLevels; ++levelIndex)
  {
   String levelFile = levelIndexes[levelIndex];
   Validate(levelFile, levelIndex);
  }
 }

 private void Validate(String levelFile, Int32 levelIndex)
 {
  String levelPath = GetPath(Constants.LEVELS_DIRECTORY, levelFile + ".txt");
  IList<String> levelData = fileManager.LoadTxt(levelPath);
  level = new Level(levelData);
  level.Validate();

  String text = String.Format("Level #{0} = {1}.txt [{2} rows]", (levelIndex + 1), levelFile, levelData.Count);
  Console.WriteLine(text);
 }
}
Download code sample here.

Summary
The revised Platformer starter kit demonstrates how to integrate an unlimited number of levels using data driven design: simply update the text file to add more levels without constant need to recompile and validate all data quickly and efficiently through system tests.

In addition to complex text files, complex XML files may also be loaded into a game, for example, to build game objects using component based design.

This will be the topic in the next post.

Wednesday, August 1, 2012

XNA and System Testing

In the previous post, we discussed the topic of XNA and Data Driven Design.
Now, let's build on this information to discuss System Testing with XNA.

Note: this post includes complete code sample on CodePlex.
Download code sample here.

System Testing
System Testing is the practice in which complete, end-to-end software is tested to evaluate the system’s compliance with its specified requirements.

Game development that implements data driven design may find system testing effective because it can be used to validate all data before fully integrating into the game.

When experimenting with new ideas, all changes made to game data must be valid, otherwise it may be possible to easily break the system, or have the system behave in an unpredictable fashion.

System testing allows for this feedback quickly, efficiently and without need to execute the entire game.

Here is a common workflow to integrate system testing into game development:
  • Write game code and edit game data
  • Run system tests and validate
  • Launch game with current data
  • Update any game data as necessary
  • Re-run system tests and validate
  • Hot swap updated game data
  • Repeat process: no constant build / deploy
Example
As an example, let’s build a basic snake simulation to demonstrate XNA and System Testing.
First, create a system that can parse text files on demand (not just at start up):
public interface IFileManager
{
 IList<String> LoadTxt(String file);
 T LoadXml<T>(String file);
 XElement LoadXElement(String file);
}
Next, create a component to manage all data that will be loaded into the game:
public interface IDataManager
{
 AllData LoadData(String[] files);
}
Sample
The following code sample builds a simulation to integrate an unlimited number of snakes into a maze using data driven design and validates each snake through system tests.
First, define a simple text file that will store all 3x possible maze sizes: Small, Medium, and Large.
Next, define an XML file that stores an unlimited number of snakes available; each snake defines:
  • Start position in maze and direction
  • Length of the snake tail
  • Time delay on each tile
  • No. tiles before direction change
<?xml version="1.0"?>
<ArrayOfSnakeData>
 <SnakeData>
  <StartPosX>10</StartPosX>
  <StartPosY>5</StartPosY>
  <Direction>Left</Direction>
  <TailLength>20</TailLength>
  <TimeOnTile>50</TimeOnTile>
  <TileToMove>10</TileToMove>
 </SnakeData>
</ArrayOfSnakeData>
Finally, write system tests to validate all data before fully integrating into the game.
Note: an IoC Container will be used to construct all components used throughout.

FILE MANAGER TESTS
[TestFixture]
public class FileManagerTests
{
 // System under test.
 private IFileManager fileManager;
 
 [SetUp]
 public void SetUp()
 {
  fileManager = IoCContainer.Resolve<IFileManager>();
 }

 [Test]
 public void BoardDataTest()
 {
  String boardFile = GetPath("BoardData.txt");

  IList<String> lines = fileManager.LoadTxt(boardFile);
  String line = lines[0];

  GameSize gameSize = (GameSize)Enum.Parse(typeof(GameSize), line, true);
  Assert.AreEqual(GameSize.Small, gameSize);
 }

 [Test]
 public void SnakeDataTest()
 {
  String snakeFile = GetPath("SnakeData.xml");
  SnakeData[] snakeData = fileManager.LoadXml<SnakeData[]>(snakeFile);
  Assert.AreEqual(1, snakeData.Length);

  SnakeData snake = snakeData[0];
  Assert.AreEqual(10, snake.StartPosX);
  Assert.AreEqual(5, snake.StartPosY);
  Assert.AreEqual(Direction.Left, snake.Direction);
  Assert.AreEqual(20, snake.TailLength);
  Assert.AreEqual(50, snake.TimeOnTile);
  Assert.AreEqual(10, snake.TileToMove);
 }
}
DATA MANAGER TESTS
[TestFixture]
public class DataManagerTests
{
 // System under test.
 private IDataManager dataManager;

 [SetUp]
 public void SetUp()
 {
  dataManager = IoCContainer.Resolve<IDataManager>();
 }

 [Test]
 public void AllDataTest()
 {
  String boardFile = GetPath("BoardData.txt");
  String snakeFile = GetPath("SnakeData.xml");

  String[] files = new[] { boardFile, snakeFile };
  AllData data = dataManager.LoadData(files);

  Assert.AreEqual(GameSize.Small, data.GameSize);
  Assert.AreEqual(1, data.SnakeData.Length);
  
 }
}
Download code sample here.

Summary
The snake simulation demonstrates how to integrate an unlimited number of snakes into a maze using data driven design: simply update the XML file to add more snakes without constant need to recompile and validate all data quickly and efficiently through system tests.

In reality, more complex text files will be loaded into a game, for example, to build game levels. Level data may require more complex validation to ensure all data is true and correct, and that all rules are observed before the level is actually loaded into the game.

This will be the topic in the next post.