3
\$\begingroup\$

I been working on a game in Unity that reads its story (Actions, scene descriptions, NPC's, ect...) from an XML and have been wondering there was a better way to store actions based on player data such as item in an inventory or attribute.

An example would be the player having a light source greater then 5 on hand while entering a dark room.

<scene>
 <description>You enter a dark room </description>
 <sceneReq>
  <text>and do not have enough light to see it well.</text>
  <action trigger="player.lightLevel > 4"  sceneIndex="5"/>
 </sceneReq>
</scene>

How would I be able to act on the above logic in C# without hard coding the actions and load dynamically from the XML? Also how should I handle logic in XML if it requires multiple actions or parameters?

For example having a crowbar and the strength greater then 15 to open a door.

<scene>
 <description>You see a stuck door in front of you.</description>
 <sceneReq>
  <text>Your effort to remove the door has failed.</text>
  <and>
   <action trigger="player.item == 1"  sceneIndex="10"/>
   <action trigger="player.strLevel > 15"  sceneIndex="10"/>
  </and>
 </sceneReq>
</scene>

Because I am rather new to parsing XML I can't think of a way to break the logic out of the XML and use it in code dynamically. I looked over https://stackoverflow.com/questions/372915/game-logic-in-xml-files and really did not seem to answer my question on how to solve it in C#.

Edit: Spent more time on this and with Blue's answer I was able to come up with a viable solution.

Sample XML

 <scene>
  <description>You enter a dark room </description>
  <condition type="itemEquip" itemType="Flashlight">
   <message>and do you do not have a light source.</message>
  </condition>
  <button text="Use flashlight" sceneIndex="2">
   <condition type="itemHave" itemType="Flashlight">
    <message> and you forgot your flashlight at home.</message>
   </condition>
  </button>
  <button text="Go back" sceneIndex="0"/>
 </scene>

Made it so that you can have a condition for a button and for the scene as separate checks, that way buttons can be also dynamic based on the condition class.

The condition class:

public enum ConditionType
{
    itemLevelReq,
    itemEquip,
    itemHas,
    playerLevel,
    playerAttribute,
    baseDefault
}

abstract class Condition
{
    public ConditionType type;
    public string failMessage = "Condition not met";

    public abstract bool IsThisConditionMet();
}

class BaseCondition : Condition
{
    ConditionType type = ConditionType.baseDefault;
    string failMessage = "Condition not met";

    override public bool IsThisConditionMet()
    {
        return false;
    }
}

//example concrete condition
class ItemHasCondition : Condition
{
    ConditionType type = ConditionType.itemHas;
    string itemType;
    int levelReq;

    public ItemHasCondition(String item, string fail)
    {
        this.itemType = item;
        this.failMessage = fail;
    }

    override public bool IsThisConditionMet()
    {
        if(Program.testPlayer.items.Contains(this.itemType)){
            return true;
        } else {
            return false;
        }
    }
}

Then here is the parser for the XML(Simplified)

private static void readThisXML()
{
    XDocument doc = XDocument.Load("Scenes.xml");

    foreach (XElement root in doc.Root.Elements("scene"))
    {
        SceneStruct scene = new SceneStruct();
        List<ButtonStruct> buttons = new List<ButtonStruct>();
        List<Condition> conditions = new List<Condition>();

        foreach (XElement element in root.Elements("button"))
        {
            ButtonStruct btn = new ButtonStruct();
            List<int> btnIndex = new List<int>();
            List<Condition> btnConditions = new List<Condition>();
            btn.text = (string)element.Attribute("text");
            //Maybe different indexes based on conditions?
            btnIndex.Add((int)element.Attribute("sceneIndex"));
            btn.btnIndex = btnIndex;

            foreach (XElement elementC in element.Elements("condition"))
            {
                if (elementC.Attribute("type").Value.Equals("itemEquip"))
                {
                    Condition con = new ItemHasCondition((string)elementC.Attribute("itemType"), (string)elementC.Element("message").Value);
                    btnConditions.Add(con);
                }
            }
            btn.conditions = btnConditions;

            buttons.Add(btn);
        }

        dwStruct.buttons = buttons;

        foreach (XElement element in root.Elements("description"))
        {
            scene.description = (string)element.Value;
        }

        foreach (XElement element in root.Elements("condition"))
        {
            if (element.Attribute("type").Value.Equals("itemEquip"))
            {
                Condition con = new ItemHasCondition((string)element.Attribute("itemType"), (string)element.Element("message").Value);
                conditions.Add(con);
            }
        }

        scene.conditions = conditions;

        sceneList.Add(scene);
    }
}

Hopefully this helps anyone who happens to stumble over this problem like I did. You might not need the enum but it could be handy for later.

\$\endgroup\$
  • \$\begingroup\$ You can't get around needing a bridge in your running program that will parse the static XML elements and decide what code to run. XML is just a way to organize data, just like flat files or a SQL database, you can't really script with it directly. \$\endgroup\$ – Patrick Hughes Dec 31 '13 at 22:40
  • \$\begingroup\$ @Patrick Hughes Any suggestions on what to use then other then XML? \$\endgroup\$ – MarcusM Jan 3 '14 at 14:34
  • \$\begingroup\$ The most common way is to have the XML reference some script entry point in your main program and pass it parameters to run. Yes, it means that you need something in your program that can turn a bunch of text strings into a function call(s) with parameters. The second most common way is what HTML pages do with JavaScript, include a block labeled as script that your main program can interpret (this is more elaborate, obviously). This is all because XML is a "data description" language and not a a "logic description" language. \$\endgroup\$ – Patrick Hughes Jan 3 '14 at 21:00
3
\$\begingroup\$

I would look heavily into state machines and deserializing the XML data into state instances.

You can create class definition for a condition, be that an integer on an item for it's int brightness = 4; value or a string name = "torch"; in the inventory.

Something along these lines:

public enum ConditionType{
    default,
    itemLevelReq,
    itemEquip,
    playerLevel,
    playerAttribute
}

abstract class Condition{

    abstract ConditionType type;
    string failMessage = "Condition not met";

    public abstract bool IsThisConditionMet();

}

//example concrete condition
class ItemLevelCondition : Condition{

    Condition type = itemLevelReq;
    string itemType;
    int levelReq;

    public ItemLevelCondition(String item, int level, string fail){
        this.itemType = item;
        this.levelReq = level;
        this.failMessage = fail;
    }

    public bool IsThisConditionMet(){
        if(player.items.contains(this.itemType)){
            return player.items[itemType].level >= this.levelReq;
        } else {
            return false;
        }
    }

}

class Scene{
     string description;
     Condition[] requirements;

     public Scene(string description,Condition[] requirements)
     {
         this.description = description;
         this.requirements = requirements;
     }

     public bool AreAllConditionsMet(){
         bool result = true;
         string responseMessage;
         foreach(Condition condition in requirements){
             if(!condition.IsThisConditionMet()){
                 responseMessage += condition.failMessage + "\n";
             }
             result &= condition.IsThisConditionMet();
         }

         Scene.OutputFailMessage();
         return result;
     }

     public void OutputFailMessage(){
         Console.Write(this.responseMessage);
     }
}

Please excuse the roughness but I hope this gives you a decent idea for structure. Formatting your XML a little better you can then create a parser to create the scenes and the requirements in them, here is an example of both the scenes above in one:

<scene index="5">
 <description>You enter a dark room and feel a door stuck in front of you</description>
 <condition type="itemLevelReq" itemType="light" levelReq="5">
  <failMessage>and do not have enough light to see it well.</text>
 </condition>
 <condition type="playerAttribute" attrType="strength" levelReq="15">
  <failMessage>You are not strong enough.</text>
 </condition>
 <condition type="itemEquip" itemType="crowbar" equipped="true">
  <failMessage>You do not have a crowbar equipped.</text>
 </condition>
</scene>

Here is a snippet of how to do the XML parsing itself:

using System.Collections.Generic;
using System.Xml;
    public static List<Scene> InitialiseScenes()
    {
        List<Scene> scenes = new List<Scene>();

        Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("Scenes.xml");

        //This if block verifies the file was loaded into the stream and throws a FileNotFound if it isn't
        if (stream != null)
        {
            StreamReader reader = new StreamReader(stream);

            XmlDocument sceneDoc = new XmlDocument();
            sceneDoc.LoadXml(reader.ReadToEnd());

            string xpathQuery = "/scenes/scene";
            foreach (XmlNode sceneNode in sceneDoc.SelectNodes(xpathQuery))
            {
                //Set up new Champion details
                Scene currentScene;
                List<Condition> conditions = InitialiseConditions(sceneNode.SelectNodes("conditions"));

                currentScene = new Scene(sceneNode.SelectNode("description").InnerText, conditions)

                scenes.Add(currentScene);
            }
        }
        else
        {
            throw new FileNotFoundException("The resource file for the champions data is missing.", "Champions.xml");
        }
        return champions;
    }

    public List<Condition> InitialiseConditions(XmlNodeList nodeList) { 

        //Get conditions from nodes
        List<Condition> result;
        foreach (XmlNode conditionNode in nodeList){
            Condition tmpCondition;

            //TODO: Create list of nodes and use switch statement to load the
            // node into a concrete implementation of the abstract Condition class
            //eg. tmpCondition = new ItemLevelCondition(item, level, fail);

            result.Add(tmpCondition);
        }
    }

I hope this gets you on the right track.

\$\endgroup\$
  • \$\begingroup\$ Hey thanks! I will give this a look over and try to get it to work out. \$\endgroup\$ – MarcusM Jan 6 '14 at 17:02
  • \$\begingroup\$ @MarcusM please make sure you let us know! We always like a good challenge/success story \$\endgroup\$ – Tom 'Blue' Piddock Jan 6 '14 at 17:10
  • \$\begingroup\$ Just edited my post to reflect your answer. Thanks for the help! \$\endgroup\$ – MarcusM Feb 24 '14 at 19:12
  • \$\begingroup\$ Glad I could help! \$\endgroup\$ – Tom 'Blue' Piddock Feb 25 '14 at 9:41

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.