Fall Fury: Part 6 - Rendering Level Elements

Rendering Level Elements

In the previous article, you learned how to build level XML files in order to create playable levels. This article discusses how the internal parser works and how XML nodes become items on the game screen.  Check out the video for this article at http://channel9.msdn.com/Series/FallFury/Part-6-Rendering-Level-Elements.  For a complete, offline version of this series, you may download a nicely formatted PDF of all the articles.

Project Interop – The C# Parser

Thanks to classes such as XmlSerializer and XDocument, parsing XML in .NET Framework is not a complicated task. Similarly, in the WinRT world there are alternatives such as Windows::Data::Xml::Dom. That being said, using the parser skeleton in C# this project leverages the existing codebase, adapting it to the specific level reading needs.

Start by creating a new WinRT project that is the part of the existing solution. Make sure that you create a Windows Runtime Component:

clip_image002

There are major differences between the .NET and WinRT stacks, especially when it comes to building code that can be invoked from any potential WinRT project type, as is the case here. Make sure that the output of the newly created project is a Windows Runtime Component (WinMD file):

clip_image004

The XMLReader project reads much more than the level data—it also manages internal XML metadata such as user scores. This article, however, focuses only on level-related aspects of the engine.

Reader is the one core class used here. It contains the ReadXml method, which receives the file name and the type of the XML file to be read. Regardless of the name, the XML file type determines the parsing rules, the root lookup location, and the produced output. The tier (level set) and individual level data are stored internally in the application folder, and the score metadata is outside the sandbox in the application data folder:

async Task<Object> ReadXml(string fileName, XmlType type)
{
        StorageFile file = null;
        StorageFolder folder = null;
    
        if (type == XmlType.LEVEL || type == XmlType.TIERS)
        {
                folder = Windows.ApplicationModel.Package.Current.InstalledLocation;
                folder = await folder.GetFolderAsync("Levels");
        }
        else if (type == XmlType.HIGHSCORE)
        {
                folder = ApplicationData.Current.LocalFolder;
                folder = await folder.GetFolderAsync("Meta");
        }
    
        file = await folder.GetFileAsync(fileName);
    
        Object returnValue = null;
        string data;
    
        using (IRandomAccessStream readStream = await file.OpenAsync(FileAccessMode.Read))
        {
                using (Stream inStream = Task.Run(() => readStream.AsStreamForRead()).Result)
                {
                        using (StreamReader reader = new StreamReader(inStream))
                        {
                                data = reader.ReadToEnd();
                        }
                }
        }
    
        if (type == XmlType.LEVEL)
        {
                returnValue = ExtractLevelData(data);
        }
        else if (type == XmlType.HIGHSCORE)
        {
                returnValue = ExtractScoreData(data);
        }
        else if (type == XmlType.TIERS)
        {
                returnValue = ExtractTierData(data);
        }
    
        return returnValue;
}

Before the level data is extracted, FallFury needs to know about the existing tiers pointing to those levels. That’s where ExtractTierData, an internal method that accepts a raw XML string and return a TierSet instance, comes in:

public sealed class TierSet
{
    public Tier[] Tiers { get; set; }
}

A Tier class has the following structure:

public sealed class Tier
{
    public string Name { get; set; }
    public string[] LevelNames { get; set; }
    public string[] LevelFiles { get; set; }
}

I am using the most fundamental types – arrays instead of generic collections, to maximize the portability of the code. The ExtractTierData method handles the XML-to-Object transformation:

TierSet ExtractTierData(string tierString)
{
        TierSet set = new TierSet();
    
        XDocument document = XDocument.Parse(tierString);
    
        set.Tiers = new Tier[document.Root.Elements().Count()];
    
        int tierCounter = 0;
    
        foreach (XElement element in document.Root.Elements())
        {
                Tier tier = new Tier();
                tier.Name = element.Attribute("name").Value;
        
                tier.LevelFiles = new string[element.Elements("level").Count()];
                tier.LevelNames = new string[element.Elements("level").Count()];
        
                
                int count = 0;
                foreach (XElement lElement in element.Elements("level"))
                {
                        tier.LevelFiles[count] = lElement.Attribute("file").Value;
                        tier.LevelNames[count] = lElement.Attribute("name").Value;
                        count++;
                }
        
                set.Tiers[tierCounter] = tier;
                tierCounter++;
        }
    
        return set;
}

I am running a coupled array block here, utilizing one array for level names and another for file locations. This is a very basic implementation of a key-value pair that depends on code-based coupling. ExtractTierData, however, is not exposed publicly and the C++ layer of FallFury will not access it because TierSet is not exposed through a compatible async method. Looking back at ReadXml, might seem like the answer to this problem. But a Task<Object> is not an interop-compatible type, which means I use ReadXmlAsync, which proxies the call through an IAsyncOperation:

public IAsyncOperation<Object> ReadXmlAsync(string filename, XmlType type)
{
    return (IAsyncOperation<Object>)AsyncInfo.Run((CancellationToken token) => ReadXml(filename, type));
}

AsyncInfo.Run starts a WinRT async operation and handles the returned result, regardless of the selected file path or type.

Level-based data is extracted in a similar manner to the tier data. The difference lies in the used internal models, as well as the node reading order:

Level ExtractLevelData(string levelString)
{
        XDocument document = XDocument.Parse(levelString);
    
        Level level = new Level();
        level.LevelMeta = new Meta();
     
        level.LevelMeta.Score = Convert.ToInt32(document.Root.Element("meta").Attribute("score").Value);
        level.LevelMeta.ButtonPrice = Convert.ToInt32(document.Root.Element("meta").Attribute("buttonPrice").Value)
        level.LevelMeta.LevelType = (LevelType)Convert.ToInt32(document.Root.Attribute("type").Value);
    
        int count = document.Root.Element("obstacles").Elements().Count();
        level.Obstacles = new Obstacle[count];
    
        count = 0;
        foreach (XElement element in document.Root.Element("obstacles").Elements())
        {
                Obstacle obstacle = new Obstacle();
                obstacle.HealthDamage = Convert.ToSingle(element.Attribute("healthDamage").Value);
                obstacle.InflictsDamage = Convert.ToBoolean(element.Attribute("inflictsDamage").Value);
                obstacle.Rotation = Convert.ToSingle(element.Attribute("rotation").Value);
                obstacle.Scale = Convert.ToSingle(element.Attribute("scale").Value);
                obstacle.X = Convert.ToSingle(element.Attribute("x").Value);
                obstacle.Y = Convert.ToSingle(element.Attribute("y").Value);
                obstacle.Type = (ObstacleType)Convert.ToInt32(element.Attribute("type").Value);
                level.Obstacles[count] = obstacle;
                count++;
        }
    
        count = document.Root.Element("monsters").Elements().Count();
        level.Monsters = new Monster[count];
    
        count = 0;
        foreach (XElement element in document.Root.Element("monsters").Elements())
        {
                Monster monster = new Monster();
                monster.CriticalDamage = Convert.ToSingle(element.Attribute("criticalDamage").Value);
                monster.Damage = Convert.ToSingle(element.Attribute("damage").Value);
                monster.DefaultAmmo = Convert.ToInt32(element.Attribute("defaultAmmo").Value);
                monster.MaxHealth = Convert.ToSingle(element.Attribute("maxHealth").Value);
                monster.X = Convert.ToSingle(element.Attribute("x").Value);
                monster.Y = Convert.ToSingle(element.Attribute("y").Value);
                monster.VelocityX = Convert.ToSingle(element.Attribute("velocityX").Value);
                monster.Lifetime = Convert.ToSingle(element.Attribute("lifetime").Value);
                monster.Type = (MonsterType)Convert.ToInt32(element.Attribute("type").Value);
                monster.Bonus = Convert.ToInt32(element.Attribute("bonus").Value);
                monster.Scale = Convert.ToSingle(element.Attribute("scale").Value);
        
                level.Monsters[count] = monster;
                count++;
        }
    
        count = document.Root.Element("buttons").Elements().Count();
        level.Buttons = new Button[count];
    
        count = 0;
        foreach (XElement element in document.Root.Element("buttons").Elements())
        {
                Button button = new Button();
        
                button.X = Convert.ToSingle(element.Attribute("x").Value);
                button.Y = Convert.ToSingle(element.Attribute("y").Value);
        
                level.Buttons[count] = button;
                count++;
        }
    
        count = document.Root.Element("powerups").Elements().Count();
        level.Powerups = new Powerup[count];
    
        count = 0;
        foreach (XElement element in document.Root.Element("powerups").Elements())
        {
                Powerup powerup = new Powerup();
        
                powerup.X = Convert.ToSingle(element.Attribute("x").Value);
                powerup.Y = Convert.ToSingle(element.Attribute("y").Value);
                powerup.Category = (PowerupCategory)Convert.ToInt32(element.Attribute("category").Value);
                powerup.Type = (PowerupType)Convert.ToInt32(element.Attribute("type").Value);
                powerup.Lifespan = Convert.ToSingle(element.Attribute("lifespan").Value);
                powerup.Effect = Convert.ToSingle(element.Attribute("effect").Value);
        
                level.Powerups[count] = powerup;
                count++;
        }
    
        Bear bear = new Bear();
        XElement bearElement = document.Root.Element("bear");
        bear.CriticalDamage = Convert.ToSingle(bearElement.Attribute("criticalDamage").Value);
        bear.Damage = Convert.ToSingle(bearElement.Attribute("damage").Value);
        bear.DefaultAmmo = Convert.ToInt32(bearElement.Attribute("defaultAmmo").Value);
        bear.MaxHealth = Convert.ToSingle(bearElement.Attribute("maxHealth").Value);
        bear.StartPosition = Convert.ToSingle(bearElement.Attribute("startPosition").Value);
        bear.Velocity = Convert.ToSingle(bearElement.Attribute("velocity").Value);
    
        level.GameBear = bear;
        
        return level;
}

As a result, the Level instance has an internal counterpart in the C++ project:

public sealed class Level
{
    public Bear GameBear { get; set; }
    public Meta LevelMeta { get; set; }
    public Obstacle[] Obstacles { get; set; }
    public Monster[] Monsters { get; set; }
    public Button[] Buttons { get; set; }
    public Powerup[] Powerups { get; set; }
}

From C# to C++ - Leveraging the WinRT Component

At this point, the C#-based WinRT component is complete and can be integrated into the C++ project. This can be done in two ways: either by adding a reference to the project itself or by adding a reference to the generated WinMD file. Both methods will ultimately produce the same output, but it’s easier to debug and modify coupled projects on the go, so I went with the first option.

To add a reference to an internal Windows Runtime Component project, right click on the C++ project in Solution Explorer and select Properties. You will see a dialog like this:

clip_image006

Select the Common Properties node in the tree on the left and open the Framework and References page:

clip_image008

In the sample screen capture above, the FallFury.XMLReader project is already added as a reference. For a new project, simply click on Add New Reference and add any compatible project or third-party extension. As soon as the reference is added, the publicly exposed methods can be accessed.

In DirectXPage.cpp I have a method called LoadLevelData that allows me to load the list of the registered levels from core.xml as well as build the visual tree for the menu items, whichallows players to select a level:

void DirectXPage::LoadLevelData()
{
    Coding4Fun::FallFury::XMLReader::Reader^ reader = ref new Coding4Fun::FallFury::XMLReader::Reader();
    Windows::Foundation::IAsyncOperation<Platform::Object^>^ result = reader->ReadXmlAsync("core.xml", Coding4Fun::FallFury::XMLReader::Models::XmlType::TIERS);
    result->Completed =
        ref new AsyncOperationCompletedHandler<Platform::Object^>(this, &DirectXPage::OnLevelLoadCompleted);
}

Following the normal asynchronous pattern, as well as the structure of the method exposed in the Reader class, I am calling ReadXmlAsync and getting the result in OnLevelLoadCompleted when the operation reaches its final stage. It is worth mentioning that the associated AsyncOperationCompletedHandler is invoked even when the reading fails; therefore, the invocation of that callback does not on its own mean that the necessary data is obtained:

Here is what happens when OnLevelLoadCompleted is called:

void DirectXPage::OnLevelLoadCompleted(IAsyncOperation<Platform::Object^> ^op, AsyncStatus s)
{
    if (s == AsyncStatus::Completed)
    {
        auto set = (Coding4Fun::FallFury::XMLReader::Models::TierSet^)op->GetResults();
        
        auto tiers = set->Tiers;
        
        int levelCounter = 0;
        
        for (auto tier = tiers->begin(); tier != tiers->end(); tier++)
        {
            StackPanel^ panel = ref new StackPanel();
            
            TextBlock^ levelTierTitle = ref new TextBlock();
            levelTierTitle->Text = (*tier)->Name;
            levelTierTitle->Style = (Windows::UI::Xaml::Style^)Application::Current->Resources->Lookup("LevelSelectTierItemText");
            levelTierTitle->RenderTransform = ref new TranslateTransform();
            panel->Children->Append(levelTierTitle);
            
            levelNames = (*tier)->LevelNames;
            auto levelFiles = (*tier)->LevelFiles;
            
            int max = levelNames->Length;
            
            for(int i = 0; i < max; i++)
            {
                MenuItem^ item = ref new MenuItem();
                item->Tag = levelCounter;
                item->Label = levelNames[i];
                item->HorizontalAlignment = Windows::UI::Xaml::HorizontalAlignment::Left;
                item->OnButtonSelected += ref new MenuItem::ButtonSelected(this, &DirectXPage::OnLevelButtonSelected);
                item->RenderTransform = ref new TranslateTransform();
                
                m_renderer->Levels.Insert(levelCounter,levelFiles[i]);
                
                panel->Children->Append(item);
                levelCounter++;
            }
            
            stkLevelContainer->Items->Append(panel);
        }
    }
}

Here, AsyncStatus can also be Cancelled or Error, so checking for Complete ensures that the expected result is processed further inside the context of the callback.

As the returned TierSet exposes the Tiers array, I am simply iterating through each of the existing items to create independent tier blocks, grouped in StackPanel elements, coupled with level-specific MenuItem instances that present the user with a choice of playing a given level. The level ID is carried in the Tag property and it will be used to identify the selected button when DirectXPage::OnLevelButtonSelected is triggered:

Reading Level-specific Data

Level data is read in the GamePlayScreen once the selected menu button passes the name and level identifiers. The LoadLevelXml method is called as the screen begins to load, preparing all assets for the start of the game session:

void GamePlayScreen::LoadLevelXML()
{
    Coding4Fun::FallFury::XMLReader::Reader^ reader = ref new Coding4Fun::FallFury::XMLReader::Reader();
    
    Platform::String^ LevelName = Manager->Levels.Lookup(StaticDataHelper::CurrentLevelID);
    
    Windows::Foundation::IAsyncOperation<Platform::Object^>^ result = 
            reader->ReadXmlAsync(LevelName, Coding4Fun::FallFury::XMLReader::Models::XmlType::LEVEL);
    
        result->Completed =
            ref new AsyncOperationCompletedHandler<Platform::Object^>(this, &GamePlayScreen::OnLevelLoadCompleted);
}

The selected ID passes from the menu screen to the game screen through CurrentLevelID, an intermediary value preserved in a StaticDataHelper class. The level file name is looked up based on the ID and then passed to ReadXmlAsync with the XmlType set to LEVEL. When the load completes, OnLevelLoadCompleted is invoked, and an additional helper class— LevelDataLoader—sets up the game components based on the received data:

void GamePlayScreen::OnLevelLoadCompleted(IAsyncOperation<Platform::Object^> ^op, AsyncStatus s)
{
        if (s == AsyncStatus::Completed)
        {
                InitializeSpriteBatch();    
        m_loader = ref new BasicLoader(Manager->m_d3dDevice.Get(), Manager->m_wicFactory.Get());
        CreateBear();
        
                LevelDataLoader^ loader = ref new LevelDataLoader((Coding4Fun::FallFury::XMLReader::Models::Level^)op->GetResults(), this);
        loader->SetupBear(GameBear);
        loader->SetupObstacles(m_obstacles);
        loader->SetupMonsters(m_monsters);
        loader->SetupButtons(m_buttons, m_buttonPrice);
        loader->SetupPowerups(m_powerups);
        m_currentLevelType = (LevelType) loader->CurrentLevel->LevelMeta->LevelType;
        
        StaticDataHelper::CurrentLevel = loader->CurrentLevel;
        StaticDataHelper::ButtonsTotal = loader->CurrentLevel->Buttons->Length;
        
        LoadTextures();
        
                CreateMonster();
                CreatePowerups();
        
        IsLevelLoaded = true;
        
        GameBear->TurnRight();
        m_particleSystem.CreatePreCachedParticleSets();
        LoadSounds();
        }
}

Each object is separated in its own method, such as SetupBear or SetupObstacles. SetupBear, for example, transforms the exposed managed Bear model to a native C++ Characters::Bear one:

void LevelDataLoader::SetupBear(Bear ^gameBear)
{
        gameBear->Position = float2(GetXPosition(CurrentLevel->GameBear->StartPosition), 0);    
    gameBear->MaxHealth = CurrentLevel->GameBear->MaxHealth;
    gameBear->CurrentHealth = CurrentLevel->GameBear->MaxHealth;
    gameBear->CurrentDamage = CurrentLevel->GameBear->Damage;
    gameBear->Velocity.y = CurrentLevel->GameBear->Velocity;
    gameBear->MaxCriticalDamage = CurrentLevel->GameBear->CriticalDamage;
    gameBear->Rotation = 0.0f;
}

Conclusion

Mixing C# and C++ components is not a complicated process. Nonetheless, it comes with specific restrictions and considerations, such as the format of the publicly exposed asynchronous calls. The above C#-based level loading engine highlights the fact that WinRT interoperability allows you to leverage the languages you know best in order to efficiently accomplish project tasks.

Tags:

Follow the Discussion

Comments Closed

Comments have been closed since this content was published more than 30 days ago, but if you'd like to continue the conversation, please create a new thread in our Forums,
or Contact Us and let us know.